From fa71daa8a7b0349dcee4416311bae8ac4054000a Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 31 May 2023 14:43:29 +0800 Subject: [PATCH 001/410] fix: fix wrong implementation for /v1/models (close #128) --- controller/model.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/controller/model.go b/controller/model.go index 9825b4ab..829c795d 100644 --- a/controller/model.go +++ b/controller/model.go @@ -23,20 +23,21 @@ type OpenAIModelPermission struct { } type OpenAIModels struct { - Id string `json:"id"` - Object string `json:"object"` - Created int `json:"created"` - OwnedBy string `json:"owned_by"` - Permission OpenAIModelPermission `json:"permission"` - Root string `json:"root"` - Parent *string `json:"parent"` + Id string `json:"id"` + Object string `json:"object"` + Created int `json:"created"` + OwnedBy string `json:"owned_by"` + Permission []OpenAIModelPermission `json:"permission"` + Root string `json:"root"` + Parent *string `json:"parent"` } var openAIModels []OpenAIModels var openAIModelsMap map[string]OpenAIModels func init() { - permission := OpenAIModelPermission{ + var permission []OpenAIModelPermission + permission = append(permission, OpenAIModelPermission{ Id: "modelperm-LwHkVFn8AcMItP432fKKDIKJ", Object: "model_permission", Created: 1626777600, @@ -49,7 +50,7 @@ func init() { Organization: "*", Group: nil, IsBlocking: false, - } + }) // https://platform.openai.com/docs/models/model-endpoint-compatibility openAIModels = []OpenAIModels{ { @@ -106,15 +107,6 @@ func init() { Root: "gpt-4-32k-0314", Parent: nil, }, - { - Id: "gpt-3.5-turbo", - Object: "model", - Created: 1677649963, - OwnedBy: "openai", - Permission: permission, - Root: "gpt-3.5-turbo", - Parent: nil, - }, { Id: "text-embedding-ada-002", Object: "model", @@ -132,7 +124,10 @@ func init() { } func ListModels(c *gin.Context) { - c.JSON(200, openAIModels) + c.JSON(200, gin.H{ + "object": "list", + "data": openAIModels, + }) } func RetrieveModel(c *gin.Context) { From f19ee0535158fef38adba1f4e0c0c7605450ebc5 Mon Sep 17 00:00:00 2001 From: quzard <1191890118@qq.com> Date: Wed, 31 May 2023 15:24:40 +0800 Subject: [PATCH 002/410] fix: fetch root user's email if blank (#129) --- controller/channel-test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/channel-test.go b/controller/channel-test.go index 0d32c8c6..0ae256f9 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -134,6 +134,9 @@ func disableChannel(channelId int, channelName string, reason string) { } func testAllChannels(c *gin.Context) error { + if common.RootUserEmail == "" { + common.RootUserEmail = model.GetRootUserEmail() + } testAllChannelsLock.Lock() if testAllChannelsRunning { testAllChannelsLock.Unlock() From 5f045f8cf546d1ed927b413c038d5cbc9a95466f Mon Sep 17 00:00:00 2001 From: quzard <1191890118@qq.com> Date: Wed, 31 May 2023 15:37:59 +0800 Subject: [PATCH 003/410] fix: update docker-compose.yml (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用相对路径,更通用 --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0fef5b82..a9e1f844 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,8 @@ services: ports: - "3000:3000" volumes: - - /home/ubuntu/data/one-api:/data - - /home/ubuntu/data/one-api/logs:/app/logs + - ./data:/data + - ./logs:/app/logs # environment: # REDIS_CONN_STRING: redis://default:redispw@localhost:49153 # SESSION_SECRET: random_string From 0f6958c57a08d9676e45186443fe22967373b817 Mon Sep 17 00:00:00 2001 From: quzard <1191890118@qq.com> Date: Wed, 31 May 2023 15:51:31 +0800 Subject: [PATCH 004/410] fix: update common.RootUserEmail when root's email changed (#132) --- controller/user.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/user.go b/controller/user.go index b6241d36..46f5d869 100644 --- a/controller/user.go +++ b/controller/user.go @@ -655,6 +655,9 @@ func EmailBind(c *gin.Context) { }) return } + if user.Role == common.RoleRootUser { + common.RootUserEmail = email + } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", From 2f44aaa645f7705f5cf78a503acfb38d82272570 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 1 Jun 2023 18:15:53 +0800 Subject: [PATCH 005/410] chore: update config prompt (close #133) --- web/src/pages/Channel/EditChannel.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 22a92f05..5cf6e6a1 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -86,7 +86,9 @@ const EditChannel = () => { inputs.type === 3 && ( <> - 注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model 参数替换为你的部署名称(模型名称中的点会被剔除)。 + 注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model + 参数替换为你的部署名称(模型名称中的点会被剔除),图片演示 { onChange={handleInputChange} value={inputs.key} autoComplete='new-password' - /> + /> } { From 139624b8a448acabae33b0bc4a75b46cef2f627b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:13:36 +0800 Subject: [PATCH 006/410] chore: bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1 (#134) Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.9.0 to 1.9.1. - [Release notes](https://github.com/gin-gonic/gin/releases) - [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md) - [Commits](https://github.com/gin-gonic/gin/compare/v1.9.0...v1.9.1) --- updated-dependencies: - dependency-name: github.com/gin-gonic/gin dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 19 ++++++++++--------- go.sum | 41 ++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 0280586a..742e1755 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,12 @@ require ( github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/static v0.0.1 - github.com/gin-gonic/gin v1.9.0 - github.com/go-playground/validator/v10 v10.12.0 + github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/validator/v10 v10.14.0 github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.3.0 github.com/pkoukk/tiktoken-go v0.1.1 - golang.org/x/crypto v0.8.0 + golang.org/x/crypto v0.9.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/sqlite v1.4.3 gorm.io/gorm v1.24.0 @@ -21,11 +21,12 @@ require ( require ( github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect - github.com/bytedance/sonic v1.8.8 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.8.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -39,17 +40,17 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.3 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.7 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e6bad42f..62cdee52 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q= -github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -17,6 +17,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= @@ -29,8 +31,8 @@ github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Sw github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= -github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= -github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -43,8 +45,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= -github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -89,12 +91,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= -github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -108,8 +110,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= -github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= @@ -128,8 +130,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -142,11 +145,11 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -154,8 +157,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From 7e80e2da3a9e38ea9d19a8eacd3109135a8a7201 Mon Sep 17 00:00:00 2001 From: zaunist Date: Fri, 2 Jun 2023 14:20:40 +0800 Subject: [PATCH 007/410] fix: add a blank VERSION file (#135) --- VERSION | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..e69de29b From 333e4216d282188fe9a7fe08c8b503f8cf26dce7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 3 Jun 2023 10:11:59 +0800 Subject: [PATCH 008/410] fix: fix balance query (close #138) --- controller/channel-billing.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index e135e5fc..6009c7dd 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -73,8 +73,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { } now := time.Now() startDate := fmt.Sprintf("%s-01", now.Format("2006-01")) - //endDate := now.Format("2006-01-02") - url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, "2023-06-01") + endDate := now.Format("2006-01-02") + url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) req, err = http.NewRequest("GET", url, nil) if err != nil { return 0, err From 98f1a627f0709a38c779dfb10037ef77bbc998ad Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 3 Jun 2023 10:17:52 +0800 Subject: [PATCH 009/410] fix: fix balance query (close #138) --- controller/channel-billing.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 6009c7dd..63ec8a84 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -74,6 +74,9 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { now := time.Now() startDate := fmt.Sprintf("%s-01", now.Format("2006-01")) endDate := now.Format("2006-01-02") + if !subscription.HasPaymentMethod { + startDate = now.AddDate(0, 0, -100).Format("2006-01-02") + } url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) req, err = http.NewRequest("GET", url, nil) if err != nil { From 2847a08852634c146ff4292b95d80fddb2646c93 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 3 Jun 2023 10:53:25 +0800 Subject: [PATCH 010/410] feat: the format of key is now constant with that of OpenAI --- common/utils.go | 24 ++++++++++++++++++++++++ controller/token.go | 2 +- model/token.go | 2 +- web/src/components/TokensTable.js | 5 +++-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/common/utils.go b/common/utils.go index 4892404c..de15ce17 100644 --- a/common/utils.go +++ b/common/utils.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "html/template" "log" + "math/rand" "net" "os/exec" "runtime" @@ -133,6 +134,29 @@ func GetUUID() string { return code } +const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func GenerateKey() string { + rand.Seed(time.Now().UnixNano()) + key := make([]byte, 48) + for i := 0; i < 16; i++ { + key[i] = keyChars[rand.Intn(len(keyChars))] + } + uuid_ := GetUUID() + for i := 0; i < 32; i++ { + c := uuid_[i] + if i%2 == 0 && c >= 'a' && c <= 'z' { + c = c - 'a' + 'A' + } + key[i+16] = c + } + return string(key) +} + func GetTimestamp() int64 { return time.Now().Unix() } diff --git a/controller/token.go b/controller/token.go index 180c4259..c486e341 100644 --- a/controller/token.go +++ b/controller/token.go @@ -119,7 +119,7 @@ func AddToken(c *gin.Context) { cleanToken := model.Token{ UserId: c.GetInt("id"), Name: token.Name, - Key: common.GetUUID(), + Key: common.GenerateKey(), CreatedTime: common.GetTimestamp(), AccessedTime: common.GetTimestamp(), ExpiredTime: token.ExpiredTime, diff --git a/model/token.go b/model/token.go index ef2d914b..4adf42e5 100644 --- a/model/token.go +++ b/model/token.go @@ -11,7 +11,7 @@ import ( type Token struct { Id int `json:"id"` UserId int `json:"user_id"` - Key string `json:"key" gorm:"type:char(32);uniqueIndex"` + Key string `json:"key" gorm:"type:char(48);uniqueIndex"` Status int `json:"status" gorm:"default:1"` Name string `json:"name" gorm:"index" ` CreatedTime int64 `json:"created_time" gorm:"bigint"` diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index fa77a32d..0a48a2ed 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -238,11 +238,12 @@ const TokensTable = () => { size={'small'} positive onClick={async () => { - if (await copy(token.key)) { + let key = "sk-" + token.key; + if (await copy(key)) { showSuccess('已复制到剪贴板!'); } else { showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); - setSearchKeyword(token.key); + setSearchKeyword(key); } }} > From 1e1c6a828f0d14b921fd0946916b0ec344719509 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 3 Jun 2023 11:09:14 +0800 Subject: [PATCH 011/410] fix: prompt user the feat is not implemented (#125) --- controller/channel-billing.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 63ec8a84..5ab0ac7b 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -40,10 +40,14 @@ type OpenAIUsageResponse struct { func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := common.ChannelBaseURLs[channel.Type] switch channel.Type { + case common.ChannelTypeOpenAI: + // do nothing case common.ChannelTypeAzure: return 0, errors.New("尚未实现") case common.ChannelTypeCustom: baseURL = channel.BaseURL + default: + return 0, errors.New("尚未实现") } url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) From 502515bbbd127ae46a37090628fb7266b0bed1bd Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 7 Jun 2023 15:31:18 +0800 Subject: [PATCH 012/410] docs: update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c972c600..6c888e62 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 在线演示 · 常见问题 + · + 赞赏支持

> **Warning**:从 `v0.2` 版本升级到 `v0.3` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.2-v0.3.sql)。 From 2ad22e14256264f0787f5c82d7b5409d431a2d63 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 7 Jun 2023 23:26:00 +0800 Subject: [PATCH 013/410] feat: support group now (close #17, close #72, close #85, close #104, close #136) Co-authored-by: quzard <1191890118@qq.com> --- common/gin.go | 26 ++++++++++ controller/relay.go | 14 +----- middleware/distributor.go | 21 +++++++- model/ability.go | 72 ++++++++++++++++++++++++++++ model/channel.go | 37 +++++++++++--- model/main.go | 4 ++ model/redemption.go | 1 - model/token.go | 1 - model/user.go | 6 +++ router/api-router.go | 1 + router/relay-router.go | 8 +++- web/src/components/ChannelsTable.js | 12 ++++- web/src/components/UsersTable.js | 13 ++++- web/src/helpers/render.js | 9 ++++ web/src/pages/Channel/EditChannel.js | 39 ++++++++++++++- 15 files changed, 235 insertions(+), 29 deletions(-) create mode 100644 common/gin.go create mode 100644 model/ability.go diff --git a/common/gin.go b/common/gin.go new file mode 100644 index 00000000..ffa1e218 --- /dev/null +++ b/common/gin.go @@ -0,0 +1,26 @@ +package common + +import ( + "bytes" + "encoding/json" + "github.com/gin-gonic/gin" + "io" +) + +func UnmarshalBodyReusable(c *gin.Context, v any) error { + requestBody, err := io.ReadAll(c.Request.Body) + if err != nil { + return err + } + err = c.Request.Body.Close() + if err != nil { + return err + } + err = json.Unmarshal(requestBody, &v) + if err != nil { + return err + } + // Reset request body + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + return nil +} diff --git a/controller/relay.go b/controller/relay.go index 81497d81..fb3b8bc4 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -116,20 +116,10 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { consumeQuota := c.GetBool("consume_quota") var textRequest GeneralOpenAIRequest if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM { - requestBody, err := io.ReadAll(c.Request.Body) + err := common.UnmarshalBodyReusable(c, &textRequest) if err != nil { - return errorWrapper(err, "read_request_body_failed", http.StatusBadRequest) + return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest) } - err = c.Request.Body.Close() - if err != nil { - return errorWrapper(err, "close_request_body_failed", http.StatusBadRequest) - } - err = json.Unmarshal(requestBody, &textRequest) - if err != nil { - return errorWrapper(err, "unmarshal_request_body_failed", http.StatusBadRequest) - } - // Reset request body - c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) } baseURL := common.ChannelBaseURLs[channelType] requestURL := c.Request.URL.String() diff --git a/middleware/distributor.go b/middleware/distributor.go index 357849e7..624cf3b1 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -9,6 +9,10 @@ import ( "strconv" ) +type ModelRequest struct { + Model string `json:"model"` +} + func Distribute() func(c *gin.Context) { return func(c *gin.Context) { var channel *model.Channel @@ -48,8 +52,21 @@ func Distribute() func(c *gin.Context) { } } else { // Select a channel for the user - var err error - channel, err = model.GetRandomChannel() + var modelRequest ModelRequest + err := common.UnmarshalBodyReusable(c, &modelRequest) + if err != nil { + c.JSON(200, gin.H{ + "error": gin.H{ + "message": "无效的请求", + "type": "one_api_error", + }, + }) + c.Abort() + return + } + userId := c.GetInt("id") + userGroup, _ := model.GetUserGroup(userId) + channel, err = model.GetRandomSatisfiedChannel(userGroup, modelRequest.Model) if err != nil { c.JSON(200, gin.H{ "error": gin.H{ diff --git a/model/ability.go b/model/ability.go new file mode 100644 index 00000000..1270ea8a --- /dev/null +++ b/model/ability.go @@ -0,0 +1,72 @@ +package model + +import ( + "one-api/common" + "strings" +) + +type Ability struct { + Group string `json:"group" gorm:"type:varchar(32);primaryKey;autoIncrement:false"` + Model string `json:"model" gorm:"primaryKey;autoIncrement:false"` + ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"` + Enabled bool `json:"enabled" gorm:"default:1"` +} + +func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { + if group == "default" { + return GetRandomChannel() + } + ability := Ability{} + var err error = nil + if common.UsingSQLite { + err = DB.Where("`group` = ? and model = ? and enabled = 1", group, model).Order("RANDOM()").Limit(1).First(&ability).Error + } else { + err = DB.Where("`group` = ? and model = ? and enabled = 1", group, model).Order("RAND()").Limit(1).First(&ability).Error + } + if err != nil { + return nil, err + } + channel := Channel{} + err = DB.First(&channel, "id = ?", ability.ChannelId).Error + return &channel, err +} + +func (channel *Channel) AddAbilities() error { + models_ := strings.Split(channel.Models, ",") + abilities := make([]Ability, 0, len(models_)) + for _, model := range models_ { + ability := Ability{ + Group: channel.Group, + Model: model, + ChannelId: channel.Id, + Enabled: channel.Status == common.ChannelStatusEnabled, + } + abilities = append(abilities, ability) + } + return DB.Create(&abilities).Error +} + +func (channel *Channel) DeleteAbilities() error { + return DB.Where("channel_id = ?", channel.Id).Delete(&Ability{}).Error +} + +// UpdateAbilities updates abilities of this channel. +// Make sure the channel is completed before calling this function. +func (channel *Channel) UpdateAbilities() error { + // A quick and dirty way to update abilities + // First delete all abilities of this channel + err := channel.DeleteAbilities() + if err != nil { + return err + } + // Then add new abilities + err = channel.AddAbilities() + if err != nil { + return err + } + return nil +} + +func UpdateAbilityStatus(channelId int, status bool) error { + return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Update("enabled", status).Error +} diff --git a/model/channel.go b/model/channel.go index 35d65827..006a67d9 100644 --- a/model/channel.go +++ b/model/channel.go @@ -1,7 +1,6 @@ package model import ( - _ "gorm.io/driver/sqlite" "one-api/common" ) @@ -19,6 +18,8 @@ type Channel struct { Other string `json:"other"` Balance float64 `json:"balance"` // in USD BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"` + Models string `json:"models"` + Group string `json:"group" gorm:"type:varchar(32);default:'default'"` } func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { @@ -49,13 +50,12 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) { } func GetRandomChannel() (*Channel, error) { - // TODO: consider weight channel := Channel{} var err error = nil if common.UsingSQLite { - err = DB.Where("status = ?", common.ChannelStatusEnabled).Order("RANDOM()").Limit(1).First(&channel).Error + err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RANDOM()").Limit(1).First(&channel).Error } else { - err = DB.Where("status = ?", common.ChannelStatusEnabled).Order("RAND()").Limit(1).First(&channel).Error + err = DB.Where("status = ? and `group` = ?", common.ChannelStatusEnabled, "default").Order("RAND()").Limit(1).First(&channel).Error } return &channel, err } @@ -63,18 +63,35 @@ func GetRandomChannel() (*Channel, error) { func BatchInsertChannels(channels []Channel) error { var err error err = DB.Create(&channels).Error - return err + if err != nil { + return err + } + for _, channel_ := range channels { + err = channel_.AddAbilities() + if err != nil { + return err + } + } + return nil } func (channel *Channel) Insert() error { var err error err = DB.Create(channel).Error + if err != nil { + return err + } + err = channel.AddAbilities() return err } func (channel *Channel) Update() error { var err error err = DB.Model(channel).Updates(channel).Error + if err != nil { + return err + } + err = channel.UpdateAbilities() return err } @@ -101,11 +118,19 @@ func (channel *Channel) UpdateBalance(balance float64) { func (channel *Channel) Delete() error { var err error err = DB.Delete(channel).Error + if err != nil { + return err + } + err = channel.DeleteAbilities() return err } func UpdateChannelStatusById(id int, status int) { - err := DB.Model(&Channel{}).Where("id = ?", id).Update("status", status).Error + err := UpdateAbilityStatus(id, status == common.ChannelStatusEnabled) + if err != nil { + common.SysError("failed to update ability status: " + err.Error()) + } + err = DB.Model(&Channel{}).Where("id = ?", id).Update("status", status).Error if err != nil { common.SysError("failed to update channel status: " + err.Error()) } diff --git a/model/main.go b/model/main.go index 3f6fafbf..8d55cee6 100644 --- a/model/main.go +++ b/model/main.go @@ -75,6 +75,10 @@ func InitDB() (err error) { if err != nil { return err } + err = db.AutoMigrate(&Ability{}) + if err != nil { + return err + } err = createRootAccountIfNeed() return err } else { diff --git a/model/redemption.go b/model/redemption.go index b731acf7..c3444f33 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -2,7 +2,6 @@ package model import ( "errors" - _ "gorm.io/driver/sqlite" "one-api/common" ) diff --git a/model/token.go b/model/token.go index 4adf42e5..8ce252b2 100644 --- a/model/token.go +++ b/model/token.go @@ -3,7 +3,6 @@ package model import ( "errors" "fmt" - _ "gorm.io/driver/sqlite" "gorm.io/gorm" "one-api/common" ) diff --git a/model/user.go b/model/user.go index 2ca0d6a4..23a97896 100644 --- a/model/user.go +++ b/model/user.go @@ -22,6 +22,7 @@ type User struct { VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management Quota int `json:"quota" gorm:"type:int;default:0"` + Group string `json:"group" gorm:"type:varchar(32);default:'default'"` } func GetMaxUserId() int { @@ -229,6 +230,11 @@ func GetUserEmail(id int) (email string, err error) { return email, err } +func GetUserGroup(id int) (group string, err error) { + err = DB.Model(&User{}).Where("id = ?", id).Select("`group`").Find(&group).Error + return group, err +} + func IncreaseUserQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") diff --git a/router/api-router.go b/router/api-router.go index 9ca2226a..abd4d23b 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -63,6 +63,7 @@ func SetApiRouter(router *gin.Engine) { { channelRoute.GET("/", controller.GetAllChannels) channelRoute.GET("/search", controller.SearchChannels) + channelRoute.GET("/models", controller.ListModels) channelRoute.GET("/:id", controller.GetChannel) channelRoute.GET("/test", controller.TestAllChannels) channelRoute.GET("/test/:id", controller.TestChannel) diff --git a/router/relay-router.go b/router/relay-router.go index 46c37a89..6d5b74a9 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -8,11 +8,15 @@ import ( func SetRelayRouter(router *gin.Engine) { // https://platform.openai.com/docs/api-reference/introduction + modelsRouter := router.Group("/v1/models") + modelsRouter.Use(middleware.TokenAuth()) + { + modelsRouter.GET("/", controller.ListModels) + modelsRouter.GET("/:model", controller.RetrieveModel) + } relayV1Router := router.Group("/v1") relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute()) { - relayV1Router.GET("/models", controller.ListModels) - relayV1Router.GET("/models/:model", controller.RetrieveModel) relayV1Router.POST("/completions", controller.RelayNotImplemented) relayV1Router.POST("/chat/completions", controller.Relay) relayV1Router.POST("/edits", controller.RelayNotImplemented) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index a0a0f5dd..be0bba16 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; +import { renderGroup } from '../helpers/render'; function renderTimestamp(timestamp) { return ( @@ -264,6 +265,14 @@ const ChannelsTable = () => { > 名称 + { + sortChannel('group'); + }} + > + 分组 + { @@ -312,6 +321,7 @@ const ChannelsTable = () => { {channel.id} {channel.name ? channel.name : '无'} + {renderGroup(channel.group)} {renderType(channel.type)} {renderStatus(channel.status)} @@ -398,7 +408,7 @@ const ChannelsTable = () => { - + diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index 1fdd8923..9906bca7 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import { API, showError, showSuccess } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; -import { renderText } from '../helpers/render'; +import { renderGroup, renderText } from '../helpers/render'; function renderRole(role) { switch (role) { @@ -175,6 +175,14 @@ const UsersTable = () => { > 用户名 + { + sortUser('group'); + }} + > + 分组 + { @@ -231,6 +239,7 @@ const UsersTable = () => { hoverable /> + {renderGroup(user.group)} {user.email ? renderText(user.email, 30) : '无'} {user.quota} {renderRole(user.role)} @@ -306,7 +315,7 @@ const UsersTable = () => { - + diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 20bfedd5..1817feb1 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -1,6 +1,15 @@ +import { Label } from 'semantic-ui-react'; + export function renderText(text, limit) { if (text.length > limit) { return text.slice(0, limit - 3) + '...'; } return text; +} + +export function renderGroup(group) { + if (group === "") { + return + } + return } \ No newline at end of file diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 5cf6e6a1..05607f98 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -14,10 +14,12 @@ const EditChannel = () => { type: 1, key: '', base_url: '', - other: '' + other: '', + models: [], }; const [batch, setBatch] = useState(false); const [inputs, setInputs] = useState(originInputs); + const [modelOptions, setModelOptions] = useState([]); const handleInputChange = (e, { name, value }) => { console.log(name, value); setInputs((inputs) => ({ ...inputs, [name]: value })); @@ -27,17 +29,36 @@ const EditChannel = () => { let res = await API.get(`/api/channel/${channelId}`); const { success, message, data } = res.data; if (success) { - data.password = ''; + if (data.models === "") { + data.models = [] + } else { + data.models = data.models.split(",") + } setInputs(data); } else { showError(message); } setLoading(false); }; + + const fetchModels = async () => { + try { + let res = await API.get(`/api/channel/models`); + setModelOptions(res.data.data.map((model) => ({ + key: model.id, + text: model.id, + value: model.id, + }))); + } catch (error) { + console.error('Error fetching models:', error); + } + }; + useEffect(() => { if (isEdit) { loadChannel().then(); } + fetchModels().then(); }, []); const submit = async () => { @@ -50,6 +71,7 @@ const EditChannel = () => { localInputs.other = '2023-03-15-preview'; } let res; + localInputs.models = localInputs.models.join(",") if (isEdit) { res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); } else { @@ -137,6 +159,19 @@ const EditChannel = () => { autoComplete='new-password' /> + + + { batch ? Date: Thu, 8 Jun 2023 09:26:54 +0800 Subject: [PATCH 014/410] feat: able to manage group now --- controller/user.go | 6 ++--- model/ability.go | 4 ++-- model/channel.go | 1 + web/src/pages/Channel/EditChannel.js | 36 ++++++++++++++++++++++++---- web/src/pages/User/EditUser.js | 17 ++++++++++--- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/controller/user.go b/controller/user.go index 46f5d869..045e0841 100644 --- a/controller/user.go +++ b/controller/user.go @@ -228,7 +228,7 @@ func GetUser(c *gin.Context) { return } myRole := c.GetInt("role") - if myRole <= user.Role { + if myRole <= user.Role && myRole != common.RoleRootUser { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无权获取同级或更高等级用户的信息", @@ -326,14 +326,14 @@ func UpdateUser(c *gin.Context) { return } myRole := c.GetInt("role") - if myRole <= originUser.Role { + if myRole <= originUser.Role && myRole != common.RoleRootUser { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无权更新同权限等级或更高权限等级的用户信息", }) return } - if myRole <= updatedUser.Role { + if myRole <= updatedUser.Role && myRole != common.RoleRootUser { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无权将其他用户权限等级提升到大于等于自己的权限等级", diff --git a/model/ability.go b/model/ability.go index 1270ea8a..f3dae7ac 100644 --- a/model/ability.go +++ b/model/ability.go @@ -9,7 +9,7 @@ type Ability struct { Group string `json:"group" gorm:"type:varchar(32);primaryKey;autoIncrement:false"` Model string `json:"model" gorm:"primaryKey;autoIncrement:false"` ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"` - Enabled bool `json:"enabled" gorm:"default:1"` + Enabled bool `json:"enabled"` } func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { @@ -68,5 +68,5 @@ func (channel *Channel) UpdateAbilities() error { } func UpdateAbilityStatus(channelId int, status bool) error { - return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Update("enabled", status).Error + return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error } diff --git a/model/channel.go b/model/channel.go index 006a67d9..fdc89eb9 100644 --- a/model/channel.go +++ b/model/channel.go @@ -91,6 +91,7 @@ func (channel *Channel) Update() error { if err != nil { return err } + DB.Model(channel).First(channel, "id = ?", channel.Id) err = channel.UpdateAbilities() return err } diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 05607f98..022a9fe4 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; import { useParams } from 'react-router-dom'; -import { API, showError, showSuccess } from '../../helpers'; +import { API, showError, showInfo, showSuccess } from '../../helpers'; import { CHANNEL_OPTIONS } from '../../constants'; const EditChannel = () => { @@ -15,13 +15,15 @@ const EditChannel = () => { key: '', base_url: '', other: '', + group: 'default', models: [], }; const [batch, setBatch] = useState(false); const [inputs, setInputs] = useState(originInputs); const [modelOptions, setModelOptions] = useState([]); + const [basicModels, setBasicModels] = useState([]); + const [fullModels, setFullModels] = useState([]); const handleInputChange = (e, { name, value }) => { - console.log(name, value); setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -49,8 +51,10 @@ const EditChannel = () => { text: model.id, value: model.id, }))); + setFullModels(res.data.data.map((model) => model.id)); + setBasicModels(res.data.data.filter((model) => !model.id.startsWith("gpt-4")).map((model) => model.id)); } catch (error) { - console.error('Error fetching models:', error); + showError(error.message); } }; @@ -62,7 +66,10 @@ const EditChannel = () => { }, []); const submit = async () => { - if (!isEdit && (inputs.name === '' || inputs.key === '')) return; + if (!isEdit && (inputs.name === '' || inputs.key === '')) { + showInfo('请填写渠道名称和渠道密钥!'); + return; + } let localInputs = inputs; if (localInputs.base_url.endsWith('/')) { localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); @@ -159,9 +166,20 @@ const EditChannel = () => { autoComplete='new-password' /> + + + { options={modelOptions} /> +
+ + +
{ batch ? { wechat_id: '', email: '', quota: 0, + group: 'default' }); - const { username, display_name, password, github_id, wechat_id, email, quota } = + const { username, display_name, password, github_id, wechat_id, email, quota, group } = inputs; const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); @@ -98,7 +99,17 @@ const EditUser = () => { /> { - userId && ( + userId && <> + + + { autoComplete='new-password' /> - ) + } Date: Thu, 8 Jun 2023 09:28:06 +0800 Subject: [PATCH 015/410] fix: able to manage root user now --- web/src/components/UsersTable.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index 9906bca7..f2e8521e 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -302,7 +302,6 @@ const UsersTable = () => { size={'small'} as={Link} to={'/user/edit/' + user.id} - disabled={user.role === 100} > 编辑 From c6edb78ac9b5e1262c4dfbbc6fa1b9023c6f5ff9 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 8 Jun 2023 09:44:47 +0800 Subject: [PATCH 016/410] docs: update README --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6c888e62..8e2e48e3 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 赞赏支持

-> **Warning**:从 `v0.2` 版本升级到 `v0.3` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.2-v0.3.sql)。 - +> **Warning**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 ## 功能 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: @@ -65,16 +64,18 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 5. 支持**令牌管理**,设置令牌的过期时间和使用次数。 6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 7. 支持**通道管理**,批量创建通道。 -8. 支持发布公告,设置充值链接,设置新用户初始额度。 -9. 支持丰富的**自定义**设置, - 1. 支持自定义系统名称,logo 以及页脚。 - 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 -10. 支持通过系统访问令牌访问管理 API。 -11. 支持用户管理,支持**多种用户登录注册方式**: +8. 支持**用户分组**以及**渠道分组**。 +9. 支持渠道**设置模型列表**。 +10. 支持发布公告,设置充值链接,设置新用户初始额度。 +11. 支持丰富的**自定义**设置, + 1. 支持自定义系统名称,logo 以及页脚。 + 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 +12. 支持通过系统访问令牌访问管理 API。 +13. 支持用户管理,支持**多种用户登录注册方式**: + 邮箱登录注册以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 -12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 +14. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 ## 部署 ### 基于 Docker 进行部署 From 9301b3fed310629df055dd0c30844d3e715cb546 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 8 Jun 2023 14:09:39 +0800 Subject: [PATCH 017/410] chore: update test logic --- controller/channel-test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index 0ae256f9..b1a43eff 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -56,7 +56,7 @@ func testChannel(channel *model.Channel, request *ChatRequest) error { if err != nil { return err } - if response.Error.Message != "" || response.Error.Code != "" { + if response.Usage.CompletionTokens == 0 { return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) } return nil From 4b6adaec0b5e7a85260b87f2bee0a71094cc2226 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 8 Jun 2023 14:54:02 +0800 Subject: [PATCH 018/410] feat: support /v1/completions (close #115) --- common/model-ratio.go | 2 +- controller/model.go | 45 ++++++++++++++++++++++++++++ controller/relay.go | 66 +++++++++++++++++++++++++++++++++--------- router/relay-router.go | 2 +- 4 files changed, 100 insertions(+), 15 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index cee4559c..2b975176 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -10,7 +10,7 @@ var ModelRatio = map[string]float64{ "gpt-4-0314": 15, "gpt-4-32k": 30, "gpt-4-32k-0314": 30, - "gpt-3.5-turbo": 1, + "gpt-3.5-turbo": 1, // $0.002 / 1K tokens "gpt-3.5-turbo-0301": 1, "text-ada-001": 0.2, "text-babbage-001": 0.25, diff --git a/controller/model.go b/controller/model.go index 829c795d..9685eb82 100644 --- a/controller/model.go +++ b/controller/model.go @@ -116,6 +116,51 @@ func init() { Root: "text-embedding-ada-002", Parent: nil, }, + { + Id: "text-davinci-003", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-davinci-003", + Parent: nil, + }, + { + Id: "text-davinci-002", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-davinci-002", + Parent: nil, + }, + { + Id: "text-curie-001", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-curie-001", + Parent: nil, + }, + { + Id: "text-babbage-001", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-babbage-001", + Parent: nil, + }, + { + Id: "text-ada-001", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-ada-001", + Parent: nil, + }, } openAIModelsMap = make(map[string]OpenAIModels) for _, model := range openAIModels { diff --git a/controller/relay.go b/controller/relay.go index fb3b8bc4..f2fa2dd4 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -19,6 +19,13 @@ type Message struct { Name *string `json:"name,omitempty"` } +const ( + RelayModeUnknown = iota + RelayModeChatCompletions + RelayModeCompletions + RelayModeEmbeddings +) + // https://platform.openai.com/docs/api-reference/chat type GeneralOpenAIRequest struct { @@ -69,7 +76,7 @@ type TextResponse struct { Error OpenAIError `json:"error"` } -type StreamResponse struct { +type ChatCompletionsStreamResponse struct { Choices []struct { Delta struct { Content string `json:"content"` @@ -78,8 +85,23 @@ type StreamResponse struct { } `json:"choices"` } +type CompletionsStreamResponse struct { + Choices []struct { + Text string `json:"text"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` +} + func Relay(c *gin.Context) { - err := relayHelper(c) + relayMode := RelayModeUnknown + if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { + relayMode = RelayModeChatCompletions + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") { + relayMode = RelayModeCompletions + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") { + relayMode = RelayModeEmbeddings + } + err := relayHelper(c, relayMode) if err != nil { if err.StatusCode == http.StatusTooManyRequests { err.OpenAIError.Message = "负载已满,请稍后再试,或升级账户以提升服务质量。" @@ -110,7 +132,7 @@ func errorWrapper(err error, code string, statusCode int) *OpenAIErrorWithStatus } } -func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { +func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { channelType := c.GetInt("channel") tokenId := c.GetInt("token_id") consumeQuota := c.GetBool("consume_quota") @@ -148,8 +170,13 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { err := relayPaLM(textRequest, c) return err } - - promptTokens := countTokenMessages(textRequest.Messages, textRequest.Model) + var promptTokens int + switch relayMode { + case RelayModeChatCompletions: + promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model) + case RelayModeCompletions: + promptTokens = countTokenText(textRequest.Prompt, textRequest.Model) + } preConsumedTokens := common.PreConsumedQuota if textRequest.MaxTokens != 0 { preConsumedTokens = promptTokens + textRequest.MaxTokens @@ -245,14 +272,27 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { dataChan <- data data = data[6:] if !strings.HasPrefix(data, "[DONE]") { - var streamResponse StreamResponse - err = json.Unmarshal([]byte(data), &streamResponse) - if err != nil { - common.SysError("Error unmarshalling stream response: " + err.Error()) - return - } - for _, choice := range streamResponse.Choices { - streamResponseText += choice.Delta.Content + switch relayMode { + case RelayModeChatCompletions: + var streamResponse ChatCompletionsStreamResponse + err = json.Unmarshal([]byte(data), &streamResponse) + if err != nil { + common.SysError("Error unmarshalling stream response: " + err.Error()) + return + } + for _, choice := range streamResponse.Choices { + streamResponseText += choice.Delta.Content + } + case RelayModeCompletions: + var streamResponse CompletionsStreamResponse + err = json.Unmarshal([]byte(data), &streamResponse) + if err != nil { + common.SysError("Error unmarshalling stream response: " + err.Error()) + return + } + for _, choice := range streamResponse.Choices { + streamResponseText += choice.Text + } } } } diff --git a/router/relay-router.go b/router/relay-router.go index 6d5b74a9..759e5f60 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -17,7 +17,7 @@ func SetRelayRouter(router *gin.Engine) { relayV1Router := router.Group("/v1") relayV1Router.Use(middleware.TokenAuth(), middleware.Distribute()) { - relayV1Router.POST("/completions", controller.RelayNotImplemented) + relayV1Router.POST("/completions", controller.Relay) relayV1Router.POST("/chat/completions", controller.Relay) relayV1Router.POST("/edits", controller.RelayNotImplemented) relayV1Router.POST("/images/generations", controller.RelayNotImplemented) From e0d0674f811a8e030f89bc0407c740009b3307b7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 8 Jun 2023 15:19:55 +0800 Subject: [PATCH 019/410] fix: fix redemption code's quota not updated --- model/redemption.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/redemption.go b/model/redemption.go index c3444f33..efff2d20 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -83,7 +83,7 @@ func (redemption *Redemption) SelectUpdate() error { // Update Make sure your token's fields is completed, because this will update non-zero values func (redemption *Redemption) Update() error { var err error - err = DB.Model(redemption).Select("name", "status", "redeemed_time").Updates(redemption).Error + err = DB.Model(redemption).Select("name", "status", "quota", "redeemed_time").Updates(redemption).Error return err } From 45e9fd66e772ecfe9ce09e7914523be66dcf5fe9 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 9 Jun 2023 16:59:00 +0800 Subject: [PATCH 020/410] feat: able to check topup history & consumption history (#78, #95) --- common/constants.go | 1 + controller/log.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ model/log.go | 44 +++++++++++++++++++++++ router/api-router.go | 5 +++ 4 files changed, 136 insertions(+) create mode 100644 controller/log.go create mode 100644 model/log.go diff --git a/common/constants.go b/common/constants.go index 7c1ff298..3a8f6d77 100644 --- a/common/constants.go +++ b/common/constants.go @@ -25,6 +25,7 @@ var OptionMap map[string]string var OptionMapRWMutex sync.RWMutex var ItemsPerPage = 10 +var MaxRecentItems = 100 var PasswordLoginEnabled = true var PasswordRegisterEnabled = true diff --git a/controller/log.go b/controller/log.go new file mode 100644 index 00000000..88d2ed1f --- /dev/null +++ b/controller/log.go @@ -0,0 +1,86 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "one-api/common" + "one-api/model" + "strconv" +) + +func GetAllLogs(c *gin.Context) { + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + logType, _ := strconv.Atoi(c.Query("type")) + logs, err := model.GetAllLogs(logType, p*common.ItemsPerPage, common.ItemsPerPage) + if err != nil { + c.JSON(200, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(200, gin.H{ + "success": true, + "message": "", + "data": logs, + }) +} + +func GetUserLogs(c *gin.Context) { + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + userId := c.GetInt("id") + logType, _ := strconv.Atoi(c.Query("type")) + logs, err := model.GetUserLogs(userId, logType, p*common.ItemsPerPage, common.ItemsPerPage) + if err != nil { + c.JSON(200, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(200, gin.H{ + "success": true, + "message": "", + "data": logs, + }) +} + +func SearchAllLogs(c *gin.Context) { + keyword := c.Query("keyword") + logs, err := model.SearchAllLogs(keyword) + if err != nil { + c.JSON(200, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(200, gin.H{ + "success": true, + "message": "", + "data": logs, + }) +} + +func SearchUserLogs(c *gin.Context) { + keyword := c.Query("keyword") + userId := c.GetInt("id") + logs, err := model.SearchUserLogs(userId, keyword) + if err != nil { + c.JSON(200, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(200, gin.H{ + "success": true, + "message": "", + "data": logs, + }) +} diff --git a/model/log.go b/model/log.go new file mode 100644 index 00000000..179955b7 --- /dev/null +++ b/model/log.go @@ -0,0 +1,44 @@ +package model + +import "one-api/common" + +type Log struct { + Id int `json:"id"` + UserId int `json:"user_id" gorm:"index"` + CreatedAt int64 `json:"created_at" gorm:"bigint"` + Type int `json:"type" gorm:"index"` + Content string `json:"content"` +} + +func RecordLog(userId int, logType int, content string) { + log := &Log{ + UserId: userId, + CreatedAt: common.GetTimestamp(), + Type: logType, + Content: content, + } + err := DB.Create(log).Error + if err != nil { + common.SysError("failed to record log: " + err.Error()) + } +} + +func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) { + err = DB.Where("type = ?", logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error + return logs, err +} + +func GetUserLogs(userId int, logType int, startIdx int, num int) (logs []*Log, err error) { + err = DB.Where("user_id = ? and type = ?", userId, logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error + return logs, err +} + +func SearchAllLogs(keyword string) (logs []*Log, err error) { + err = DB.Where("type = ? or content LIKE ?", keyword, keyword+"%").Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error + return logs, err +} + +func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { + err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error + return logs, err +} diff --git a/router/api-router.go b/router/api-router.go index abd4d23b..de8249b1 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -93,5 +93,10 @@ func SetApiRouter(router *gin.Engine) { redemptionRoute.PUT("/", controller.UpdateRedemption) redemptionRoute.DELETE("/:id", controller.DeleteRedemption) } + logRoute := apiRouter.Group("/log") + logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs) + logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) + logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) + logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) } } From 813bf0bd66cb2149e0f84426ef1ad7858f52e2c7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 9 Jun 2023 18:05:51 +0800 Subject: [PATCH 021/410] refactor: enable model configuration on default group (close #143) --- README.md | 2 ++ bin/migration_v0.3-v0.4.sql | 17 +++++++++++++++++ model/ability.go | 3 --- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 bin/migration_v0.3-v0.4.sql diff --git a/README.md b/README.md index 8e2e48e3..c4d6a655 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 > **Warning**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 +> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 + ## 功能 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: + [x] OpenAI 官方通道 diff --git a/bin/migration_v0.3-v0.4.sql b/bin/migration_v0.3-v0.4.sql new file mode 100644 index 00000000..e6103c29 --- /dev/null +++ b/bin/migration_v0.3-v0.4.sql @@ -0,0 +1,17 @@ +INSERT INTO abilities (`group`, model, channel_id, enabled) +SELECT c.`group`, m.model, c.id, 1 +FROM channels c +CROSS JOIN ( + SELECT 'gpt-3.5-turbo' AS model UNION ALL + SELECT 'gpt-3.5-turbo-0301' AS model UNION ALL + SELECT 'gpt-4' AS model UNION ALL + SELECT 'gpt-4-0314' AS model +) AS m +WHERE c.status = 1 + AND NOT EXISTS ( + SELECT 1 + FROM abilities a + WHERE a.`group` = c.`group` + AND a.model = m.model + AND a.channel_id = c.id +); diff --git a/model/ability.go b/model/ability.go index f3dae7ac..26ff65d2 100644 --- a/model/ability.go +++ b/model/ability.go @@ -13,9 +13,6 @@ type Ability struct { } func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { - if group == "default" { - return GetRandomChannel() - } ability := Ability{} var err error = nil if common.UsingSQLite { From 8901f03864097c95a0107f8e3c31e42056a4812d Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 9 Jun 2023 18:30:01 +0800 Subject: [PATCH 022/410] feat: support set proxy for channel OpenAI (close #139) --- README.md | 4 ++-- controller/channel-test.go | 2 ++ controller/relay.go | 4 ++++ middleware/distributor.go | 8 +++----- web/src/pages/Channel/EditChannel.js | 14 ++++++++++++++ 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c4d6a655..08532b97 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 ## 功能 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: - + [x] OpenAI 官方通道 + + [x] OpenAI 官方通道(支持配置代理) + [x] **Azure OpenAI API** + [x] [API2D](https://api2d.com/r/197971) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) @@ -59,7 +59,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] [OpenAI Max](https://openaimax.com) + [x] [OpenAI-SB](https://openai-sb.com) + [x] [CloseAI](https://console.openai-asia.com/r/2412) - + [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理 + + [x] 自定义渠道:例如各种未收录的第三方代理服务 2. 支持通过**负载均衡**的方式访问多个渠道。 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 4. 支持**多机部署**,[详见此处](#多机部署)。 diff --git a/controller/channel-test.go b/controller/channel-test.go index b1a43eff..838a2738 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -27,6 +27,8 @@ func testChannel(channel *model.Channel, request *ChatRequest) error { } else { if channel.Type == common.ChannelTypeCustom { requestURL = channel.BaseURL + } else if channel.Type == common.ChannelTypeOpenAI && channel.BaseURL != "" { + requestURL = channel.BaseURL } requestURL += "/v1/chat/completions" } diff --git a/controller/relay.go b/controller/relay.go index f2fa2dd4..ef279e3a 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -147,6 +147,10 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { requestURL := c.Request.URL.String() if channelType == common.ChannelTypeCustom { baseURL = c.GetString("base_url") + } else if channelType == common.ChannelTypeOpenAI { + if c.GetString("base_url") != "" { + baseURL = c.GetString("base_url") + } } fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) if channelType == common.ChannelTypeAzure { diff --git a/middleware/distributor.go b/middleware/distributor.go index 624cf3b1..6fa2bc28 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -82,11 +82,9 @@ func Distribute() func(c *gin.Context) { c.Set("channel_id", channel.Id) c.Set("channel_name", channel.Name) c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) - if channel.Type == common.ChannelTypeCustom || channel.Type == common.ChannelTypeAzure { - c.Set("base_url", channel.BaseURL) - if channel.Type == common.ChannelTypeAzure { - c.Set("api_version", channel.Other) - } + c.Set("base_url", channel.BaseURL) + if channel.Type == common.ChannelTypeAzure { + c.Set("api_version", channel.Other) } c.Next() } diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 022a9fe4..798442ac 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -198,6 +198,20 @@ const EditChannel = () => { handleInputChange(null, { name: 'models', value: fullModels }); }}>填入所有模型 + { + inputs.type === 1 && ( + + + + ) + } { batch ? Date: Fri, 9 Jun 2023 18:57:27 +0800 Subject: [PATCH 023/410] fix: fix not using proxy when update balance --- controller/channel-billing.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 5ab0ac7b..de3fc5f9 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -41,7 +41,9 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := common.ChannelBaseURLs[channel.Type] switch channel.Type { case common.ChannelTypeOpenAI: - // do nothing + if channel.BaseURL != "" { + baseURL = channel.BaseURL + } case common.ChannelTypeAzure: return 0, errors.New("尚未实现") case common.ChannelTypeCustom: From 74f508e847cf270a65350bdcc256c47269287daf Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 10 Jun 2023 16:04:04 +0800 Subject: [PATCH 024/410] feat: now user can check its topup & consume history (close #78, close #95) --- controller/relay.go | 2 + model/log.go | 21 +++- model/main.go | 4 + model/redemption.go | 2 + web/src/App.js | 9 ++ web/src/components/Header.js | 5 + web/src/components/LogsTable.js | 186 ++++++++++++++++++++++++++++++++ web/src/pages/Log/index.js | 14 +++ 8 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 web/src/components/LogsTable.js create mode 100644 web/src/pages/Log/index.js diff --git a/controller/relay.go b/controller/relay.go index ef279e3a..ac68f73d 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -244,6 +244,8 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { common.SysError("Error consuming token remain quota: " + err.Error()) } + userId := c.GetInt("id") + model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度", textRequest.Model, quota)) } }() diff --git a/model/log.go b/model/log.go index 179955b7..493f5613 100644 --- a/model/log.go +++ b/model/log.go @@ -1,6 +1,9 @@ package model -import "one-api/common" +import ( + "gorm.io/gorm" + "one-api/common" +) type Log struct { Id int `json:"id"` @@ -10,6 +13,12 @@ type Log struct { Content string `json:"content"` } +const ( + LogTypeUnknown = iota + LogTypeTopup + LogTypeConsume +) + func RecordLog(userId int, logType int, content string) { log := &Log{ UserId: userId, @@ -29,7 +38,13 @@ func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) { } func GetUserLogs(userId int, logType int, startIdx int, num int) (logs []*Log, err error) { - err = DB.Where("user_id = ? and type = ?", userId, logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error + var tx *gorm.DB + if logType == LogTypeUnknown { + tx = DB.Where("user_id = ?", userId) + } else { + tx = DB.Where("user_id = ? and type = ?", userId, logType) + } + err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error return logs, err } @@ -39,6 +54,6 @@ func SearchAllLogs(keyword string) (logs []*Log, err error) { } func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { - err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error + err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error return logs, err } diff --git a/model/main.go b/model/main.go index 8d55cee6..c1562101 100644 --- a/model/main.go +++ b/model/main.go @@ -79,6 +79,10 @@ func InitDB() (err error) { if err != nil { return err } + err = db.AutoMigrate(&Log{}) + if err != nil { + return err + } err = createRootAccountIfNeed() return err } else { diff --git a/model/redemption.go b/model/redemption.go index efff2d20..155e3cfd 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -2,6 +2,7 @@ package model import ( "errors" + "fmt" "one-api/common" ) @@ -65,6 +66,7 @@ func Redeem(key string, userId int) (quota int, err error) { if err != nil { common.SysError("更新兑换码状态失败:" + err.Error()) } + RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %d 点额度", redemption.Quota)) }() return redemption.Quota, nil } diff --git a/web/src/App.js b/web/src/App.js index b2699858..e404b7f3 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -22,6 +22,7 @@ import EditChannel from './pages/Channel/EditChannel'; import Redemption from './pages/Redemption'; import EditRedemption from './pages/Redemption/EditRedemption'; import TopUp from './pages/TopUp'; +import Log from './pages/Log'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -250,6 +251,14 @@ function App() { } /> + + + + } + /> + {timestamp2string(timestamp)} + + ); +} + +function renderType(type) { + switch (type) { + case 1: + return ; + case 2: + return ; + default: + return ; + } +} + +const LogsTable = () => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + + const loadLogs = async (startIdx) => { + const res = await API.get(`/api/log/self/?p=${startIdx}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setLogs(data); + } else { + let newLogs = logs; + newLogs.push(...data); + setLogs(newLogs); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadLogs(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + const refresh = async () => { + setLoading(true); + await loadLogs(0); + }; + + useEffect(() => { + loadLogs(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const searchLogs = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadLogs(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setLogs(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (e, { value }) => { + setSearchKeyword(value.trim()); + }; + + const sortLog = (key) => { + if (logs.length === 0) return; + setLoading(true); + let sortedLogs = [...logs]; + sortedLogs.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedLogs[0].id === logs[0].id) { + sortedLogs.reverse(); + } + setLogs(sortedLogs); + setLoading(false); + }; + + return ( + <> + + + + { + sortLog('created_time'); + }} + width={3} + > + 时间 + + { + sortLog('type'); + }} + width={2} + > + 类型 + + { + sortLog('content'); + }} + width={11} + > + 详情 + + + + + + {logs + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((log, idx) => { + if (log.deleted) return <>; + return ( + + {renderTimestamp(log.created_at)} + {renderType(log.type)} + {log.content} + + ); + })} + + + + + + + + + + +
+ + ); +}; + +export default LogsTable; diff --git a/web/src/pages/Log/index.js b/web/src/pages/Log/index.js new file mode 100644 index 00000000..8c6073a0 --- /dev/null +++ b/web/src/pages/Log/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Header, Segment } from 'semantic-ui-react'; +import LogsTable from '../../components/LogsTable'; + +const Token = () => ( + <> + +
额度明细
+ +
+ +); + +export default Token; From d29c2730737544059600a835262322f8d938d6ca Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 10 Jun 2023 16:31:40 +0800 Subject: [PATCH 025/410] chore: add more log types --- controller/user.go | 4 ++++ model/log.go | 2 ++ model/user.go | 11 +++++++++-- web/src/components/LogsTable.js | 4 ++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/controller/user.go b/controller/user.go index 045e0841..09eaccd1 100644 --- a/controller/user.go +++ b/controller/user.go @@ -2,6 +2,7 @@ package controller import ( "encoding/json" + "fmt" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "net/http" @@ -351,6 +352,9 @@ func UpdateUser(c *gin.Context) { }) return } + if originUser.Quota != updatedUser.Quota { + model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %d 点修改为 %d 点", originUser.Quota, updatedUser.Quota)) + } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", diff --git a/model/log.go b/model/log.go index 493f5613..bc096055 100644 --- a/model/log.go +++ b/model/log.go @@ -17,6 +17,8 @@ const ( LogTypeUnknown = iota LogTypeTopup LogTypeConsume + LogTypeManage + LogTypeSystem ) func RecordLog(userId int, logType int, content string) { diff --git a/model/user.go b/model/user.go index 23a97896..60278a07 100644 --- a/model/user.go +++ b/model/user.go @@ -2,6 +2,7 @@ package model import ( "errors" + "fmt" "gorm.io/gorm" "one-api/common" "strings" @@ -73,8 +74,14 @@ func (user *User) Insert() error { } user.Quota = common.QuotaForNewUser user.AccessToken = common.GetUUID() - err = DB.Create(user).Error - return err + result := DB.Create(user) + if result.Error != nil { + return result.Error + } + if common.QuotaForNewUser > 0 { + RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %d 点额度", common.QuotaForNewUser)) + } + return nil } func (user *User) Update(updatePassword bool) error { diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index a27ffbc3..1c623898 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -18,6 +18,10 @@ function renderType(type) { return ; case 2: return ; + case 3: + return ; + case 4: + return ; default: return ; } From 0b4bf30908734387319bf7f3427eaf421c5a0620 Mon Sep 17 00:00:00 2001 From: JustSong <39998050+songquanpeng@users.noreply.github.com> Date: Sat, 10 Jun 2023 16:34:14 +0800 Subject: [PATCH 026/410] docs: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08532b97..d14aa5cd 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 赞赏支持

-> **Warning**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 +> **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 From 64db39320a7e81c385ed8c466b01795321f14408 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 10 Jun 2023 20:40:07 +0800 Subject: [PATCH 027/410] feat: now able to check all user's log --- model/log.go | 8 +++- web/src/components/LogsTable.js | 78 ++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/model/log.go b/model/log.go index bc096055..52d02a6f 100644 --- a/model/log.go +++ b/model/log.go @@ -35,7 +35,13 @@ func RecordLog(userId int, logType int, content string) { } func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) { - err = DB.Where("type = ?", logType).Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error + var tx *gorm.DB + if logType == LogTypeUnknown { + tx = DB + } else { + tx = DB.Where("type = ?", logType) + } + err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error return logs, err } diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 1c623898..e5ec5dfa 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Button, Label, Pagination, Table } from 'semantic-ui-react'; -import { API, showError, timestamp2string } from '../helpers'; +import { Button, Label, Pagination, Select, Table } from 'semantic-ui-react'; +import { API, isAdmin, showError, timestamp2string } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; @@ -12,6 +12,19 @@ function renderTimestamp(timestamp) { ); } +const MODE_OPTIONS = [ + { key: 'all', text: '全部用户', value: 'all' }, + { key: 'self', text: '当前用户', value: 'self' }, +]; + +const LOG_OPTIONS = [ + { key: '0', text: '全部', value: 0 }, + { key: '1', text: '充值', value: 1 }, + { key: '2', text: '消费', value: 2 }, + { key: '3', text: '管理', value: 3 }, + { key: '4', text: '系统', value: 4 } +]; + function renderType(type) { switch (type) { case 1: @@ -21,7 +34,7 @@ function renderType(type) { case 3: return ; case 4: - return ; + return ; default: return ; } @@ -33,9 +46,16 @@ const LogsTable = () => { const [activePage, setActivePage] = useState(1); const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); + const [logType, setLogType] = useState(0); + const [mode, setMode] = useState('self'); // all, self + const showModePanel = isAdmin(); const loadLogs = async (startIdx) => { - const res = await API.get(`/api/log/self/?p=${startIdx}`); + let url = `/api/log/self/?p=${startIdx}&type=${logType}`; + if (mode === 'all') { + url = `/api/log/?p=${startIdx}&type=${logType}`; + } + const res = await API.get(url); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { @@ -74,6 +94,10 @@ const LogsTable = () => { }); }, []); + useEffect(() => { + refresh().then(); + }, [mode, logType]); + const searchLogs = async () => { if (searchKeyword === '') { // if keyword is blank, load files instead. @@ -125,6 +149,19 @@ const LogsTable = () => { > 时间
+ { + showModePanel && ( + { + sortLog('user_id'); + }} + width={1} + > + 用户 + + ) + } { @@ -139,7 +176,7 @@ const LogsTable = () => { onClick={() => { sortLog('content'); }} - width={11} + width={showModePanel ? 10 : 11} > 详情 @@ -157,6 +194,11 @@ const LogsTable = () => { return ( {renderTimestamp(log.created_at)} + { + showModePanel && ( + + ) + } {renderType(log.type)} {log.content} @@ -166,7 +208,31 @@ const LogsTable = () => { - + + { + showModePanel && ( + { + setLogType(value); + }} + /> Date: Sat, 10 Jun 2023 20:43:32 +0800 Subject: [PATCH 028/410] docs: update README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d14aa5cd..82628491 100644 --- a/README.md +++ b/README.md @@ -68,16 +68,17 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 7. 支持**通道管理**,批量创建通道。 8. 支持**用户分组**以及**渠道分组**。 9. 支持渠道**设置模型列表**。 -10. 支持发布公告,设置充值链接,设置新用户初始额度。 -11. 支持丰富的**自定义**设置, +10. 支持**查看额度明细**。 +11. 支持发布公告,设置充值链接,设置新用户初始额度。 +12. 支持丰富的**自定义**设置, 1. 支持自定义系统名称,logo 以及页脚。 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 -12. 支持通过系统访问令牌访问管理 API。 -13. 支持用户管理,支持**多种用户登录注册方式**: +13. 支持通过系统访问令牌访问管理 API。 +14. 支持用户管理,支持**多种用户登录注册方式**: + 邮箱登录注册以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 -14. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 +15. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 ## 部署 ### 基于 Docker 进行部署 From 4339f45f747bfb19cb2d36e0f603ab64afeb383a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 09:37:36 +0800 Subject: [PATCH 029/410] feat: support /v1/moderations now (close #117) --- common/model-ratio.go | 4 ++-- controller/model.go | 18 ++++++++++++++++++ controller/relay.go | 12 ++++++++++++ middleware/distributor.go | 6 ++++++ router/relay-router.go | 2 +- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 2b975176..bc7e7be3 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -26,8 +26,8 @@ var ModelRatio = map[string]float64{ "ada": 10, "text-embedding-ada-002": 0.2, "text-search-ada-doc-001": 10, - "text-moderation-stable": 10, - "text-moderation-latest": 10, + "text-moderation-stable": 0.1, + "text-moderation-latest": 0.1, } func ModelRatio2JSONString() string { diff --git a/controller/model.go b/controller/model.go index 9685eb82..dd3777f7 100644 --- a/controller/model.go +++ b/controller/model.go @@ -161,6 +161,24 @@ func init() { Root: "text-ada-001", Parent: nil, }, + { + Id: "text-moderation-latest", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-moderation-latest", + Parent: nil, + }, + { + Id: "text-moderation-stable", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "text-moderation-stable", + Parent: nil, + }, } openAIModelsMap = make(map[string]OpenAIModels) for _, model := range openAIModels { diff --git a/controller/relay.go b/controller/relay.go index ac68f73d..a581d3cc 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -24,6 +24,7 @@ const ( RelayModeChatCompletions RelayModeCompletions RelayModeEmbeddings + RelayModeModeration ) // https://platform.openai.com/docs/api-reference/chat @@ -37,6 +38,7 @@ type GeneralOpenAIRequest struct { Temperature float64 `json:"temperature"` TopP float64 `json:"top_p"` N int `json:"n"` + Input string `json:"input"` } type ChatRequest struct { @@ -100,6 +102,8 @@ func Relay(c *gin.Context) { relayMode = RelayModeCompletions } else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") { relayMode = RelayModeEmbeddings + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { + relayMode = RelayModeModeration } err := relayHelper(c, relayMode) if err != nil { @@ -143,6 +147,9 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest) } } + if relayMode == RelayModeModeration && textRequest.Model == "" { + textRequest.Model = "text-moderation-latest" + } baseURL := common.ChannelBaseURLs[channelType] requestURL := c.Request.URL.String() if channelType == common.ChannelTypeCustom { @@ -180,6 +187,8 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model) case RelayModeCompletions: promptTokens = countTokenText(textRequest.Prompt, textRequest.Model) + case RelayModeModeration: + promptTokens = countTokenText(textRequest.Input, textRequest.Model) } preConsumedTokens := common.PreConsumedQuota if textRequest.MaxTokens != 0 { @@ -239,6 +248,9 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { quota = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens*completionRatio } quota = int(float64(quota) * ratio) + if ratio != 0 && quota <= 0 { + quota = 1 + } quotaDelta := quota - preConsumedQuota err := model.PostConsumeTokenQuota(tokenId, quotaDelta) if err != nil { diff --git a/middleware/distributor.go b/middleware/distributor.go index 6fa2bc28..0f4221bf 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -7,6 +7,7 @@ import ( "one-api/common" "one-api/model" "strconv" + "strings" ) type ModelRequest struct { @@ -64,6 +65,11 @@ func Distribute() func(c *gin.Context) { c.Abort() return } + if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { + if modelRequest.Model == "" { + modelRequest.Model = "text-moderation-stable" + } + } userId := c.GetInt("id") userGroup, _ := model.GetUserGroup(userId) channel, err = model.GetRandomSatisfiedChannel(userGroup, modelRequest.Model) diff --git a/router/relay-router.go b/router/relay-router.go index 759e5f60..0b697af8 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -37,6 +37,6 @@ func SetRelayRouter(router *gin.Engine) { relayV1Router.POST("/fine-tunes/:id/cancel", controller.RelayNotImplemented) relayV1Router.GET("/fine-tunes/:id/events", controller.RelayNotImplemented) relayV1Router.DELETE("/models/:model", controller.RelayNotImplemented) - relayV1Router.POST("/moderations", controller.RelayNotImplemented) + relayV1Router.POST("/moderations", controller.Relay) } } From f97a9ce597650d01c95353eabfde9f61c9e3d77a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 09:49:57 +0800 Subject: [PATCH 030/410] fix: correct OpenAI error code's type --- controller/channel-test.go | 2 +- controller/relay.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index 838a2738..ec865b23 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -59,7 +59,7 @@ func testChannel(channel *model.Channel, request *ChatRequest) error { return err } if response.Usage.CompletionTokens == 0 { - return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) + return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) } return nil } diff --git a/controller/relay.go b/controller/relay.go index a581d3cc..63b77e4a 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -65,7 +65,7 @@ type OpenAIError struct { Message string `json:"message"` Type string `json:"type"` Param string `json:"param"` - Code string `json:"code"` + Code any `json:"code"` } type OpenAIErrorWithStatusCode struct { From 9d0bec83dfbadd3917c0e37c8caa23fd59458b75 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 09:55:50 +0800 Subject: [PATCH 031/410] chore: update prompt --- controller/relay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/relay.go b/controller/relay.go index 63b77e4a..ed47ceb3 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -108,7 +108,7 @@ func Relay(c *gin.Context) { err := relayHelper(c, relayMode) if err != nil { if err.StatusCode == http.StatusTooManyRequests { - err.OpenAIError.Message = "负载已满,请稍后再试,或升级账户以提升服务质量。" + err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。" } c.JSON(err.StatusCode, gin.H{ "error": err.OpenAIError, From 596446dba4a0a0e0fe8dc34f36883188c75f4374 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 11:08:16 +0800 Subject: [PATCH 032/410] feat: able to set group ratio now (close #62, close #142) --- common/group-ratio.go | 30 ++++++++++++++++++++++++++++ controller/group.go | 19 ++++++++++++++++++ controller/relay.go | 3 ++- middleware/distributor.go | 5 +++-- model/option.go | 3 +++ router/api-router.go | 5 +++++ web/src/components/SystemSetting.js | 20 +++++++++++++++++++ web/src/helpers/render.js | 4 ++++ web/src/pages/Channel/EditChannel.js | 25 +++++++++++++++++++++-- web/src/pages/User/EditUser.js | 28 +++++++++++++++++++++++--- 10 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 common/group-ratio.go create mode 100644 controller/group.go diff --git a/common/group-ratio.go b/common/group-ratio.go new file mode 100644 index 00000000..0a9cf4ba --- /dev/null +++ b/common/group-ratio.go @@ -0,0 +1,30 @@ +package common + +import "encoding/json" + +var GroupRatio = map[string]float64{ + "default": 1, + "vip": 1, + "svip": 1, +} + +func GroupRatio2JSONString() string { + jsonBytes, err := json.Marshal(GroupRatio) + if err != nil { + SysError("Error marshalling model ratio: " + err.Error()) + } + return string(jsonBytes) +} + +func UpdateGroupRatioByJSONString(jsonStr string) error { + return json.Unmarshal([]byte(jsonStr), &GroupRatio) +} + +func GetGroupRatio(name string) float64 { + ratio, ok := GroupRatio[name] + if !ok { + SysError("Group ratio not found: " + name) + return 1 + } + return ratio +} diff --git a/controller/group.go b/controller/group.go new file mode 100644 index 00000000..2b2f6006 --- /dev/null +++ b/controller/group.go @@ -0,0 +1,19 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "net/http" + "one-api/common" +) + +func GetGroups(c *gin.Context) { + groupNames := make([]string, 0) + for groupName, _ := range common.GroupRatio { + groupNames = append(groupNames, groupName) + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": groupNames, + }) +} diff --git a/controller/relay.go b/controller/relay.go index ed47ceb3..fb4c358f 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -140,6 +140,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { channelType := c.GetInt("channel") tokenId := c.GetInt("token_id") consumeQuota := c.GetBool("consume_quota") + group := c.GetString("group") var textRequest GeneralOpenAIRequest if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM { err := common.UnmarshalBodyReusable(c, &textRequest) @@ -194,7 +195,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if textRequest.MaxTokens != 0 { preConsumedTokens = promptTokens + textRequest.MaxTokens } - ratio := common.GetModelRatio(textRequest.Model) + ratio := common.GetModelRatio(textRequest.Model) * common.GetGroupRatio(group) preConsumedQuota := int(float64(preConsumedTokens) * ratio) if consumeQuota { err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota) diff --git a/middleware/distributor.go b/middleware/distributor.go index 0f4221bf..08568ea1 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -16,6 +16,9 @@ type ModelRequest struct { func Distribute() func(c *gin.Context) { return func(c *gin.Context) { + userId := c.GetInt("id") + userGroup, _ := model.GetUserGroup(userId) + c.Set("group", userGroup) var channel *model.Channel channelId, ok := c.Get("channelId") if ok { @@ -70,8 +73,6 @@ func Distribute() func(c *gin.Context) { modelRequest.Model = "text-moderation-stable" } } - userId := c.GetInt("id") - userGroup, _ := model.GetUserGroup(userId) channel, err = model.GetRandomSatisfiedChannel(userGroup, modelRequest.Model) if err != nil { c.JSON(200, gin.H{ diff --git a/model/option.go b/model/option.go index 5f7ad36c..5e74984d 100644 --- a/model/option.go +++ b/model/option.go @@ -58,6 +58,7 @@ func InitOptionMap() { common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold) common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota) common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() + common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString() common.OptionMap["TopUpLink"] = common.TopUpLink common.OptionMapRWMutex.Unlock() loadOptionsFromDatabase() @@ -177,6 +178,8 @@ func updateOptionMap(key string, value string) (err error) { common.PreConsumedQuota, _ = strconv.Atoi(value) case "ModelRatio": err = common.UpdateModelRatioByJSONString(value) + case "GroupRatio": + err = common.UpdateGroupRatioByJSONString(value) case "TopUpLink": common.TopUpLink = value case "ChannelDisableThreshold": diff --git a/router/api-router.go b/router/api-router.go index de8249b1..062ccac1 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -98,5 +98,10 @@ func SetApiRouter(router *gin.Engine) { logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) + groupRoute := apiRouter.Group("/group") + groupRoute.Use(middleware.AdminAuth()) + { + groupRoute.GET("/", controller.GetGroups) + } } } diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 0977a804..3b40822b 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -30,6 +30,7 @@ const SystemSetting = () => { QuotaRemindThreshold: 0, PreConsumedQuota: 0, ModelRatio: '', + GroupRatio: '', TopUpLink: '', AutomaticDisableChannelEnabled: '', ChannelDisableThreshold: 0, @@ -101,6 +102,7 @@ const SystemSetting = () => { name === 'QuotaRemindThreshold' || name === 'PreConsumedQuota' || name === 'ModelRatio' || + name === 'GroupRatio' || name === 'TopUpLink' ) { setInputs((inputs) => ({ ...inputs, [name]: value })); @@ -131,6 +133,13 @@ const SystemSetting = () => { } await updateOption('ModelRatio', inputs.ModelRatio); } + if (originInputs['GroupRatio'] !== inputs.GroupRatio) { + if (!verifyJSON(inputs.GroupRatio)) { + showError('分组倍率不是合法的 JSON 字符串'); + return; + } + await updateOption('GroupRatio', inputs.GroupRatio); + } if (originInputs['TopUpLink'] !== inputs.TopUpLink) { await updateOption('TopUpLink', inputs.TopUpLink); } @@ -329,6 +338,17 @@ const SystemSetting = () => { placeholder='为一个 JSON 文本,键为模型名称,值为倍率' /> + + + 保存运营设置
diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 1817feb1..506de16f 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -10,6 +10,10 @@ export function renderText(text, limit) { export function renderGroup(group) { if (group === "") { return + } else if (group === "vip" || group === "pro") { + return + } else if (group === "svip" || group === "premium") { + return } return } \ No newline at end of file diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 798442ac..cc40bd98 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -21,6 +21,7 @@ const EditChannel = () => { const [batch, setBatch] = useState(false); const [inputs, setInputs] = useState(originInputs); const [modelOptions, setModelOptions] = useState([]); + const [groupOptions, setGroupOptions] = useState([]); const [basicModels, setBasicModels] = useState([]); const [fullModels, setFullModels] = useState([]); const handleInputChange = (e, { name, value }) => { @@ -58,11 +59,25 @@ const EditChannel = () => { } }; + const fetchGroups = async () => { + try { + let res = await API.get(`/api/group`); + setGroupOptions(res.data.data.map((group) => ({ + key: group, + text: group, + value: group, + }))); + } catch (error) { + showError(error.message); + } + }; + useEffect(() => { if (isEdit) { loadChannel().then(); } fetchModels().then(); + fetchGroups().then(); }, []); const submit = async () => { @@ -167,13 +182,19 @@ const EditChannel = () => { /> - diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index 71d15b8a..3193dafa 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -17,11 +17,24 @@ const EditUser = () => { quota: 0, group: 'default' }); + const [groupOptions, setGroupOptions] = useState([]); const { username, display_name, password, github_id, wechat_id, email, quota, group } = inputs; const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; + const fetchGroups = async () => { + try { + let res = await API.get(`/api/group`); + setGroupOptions(res.data.data.map((group) => ({ + key: group, + text: group, + value: group, + }))); + } catch (error) { + showError(error.message); + } + }; const loadUser = async () => { let res = undefined; @@ -41,6 +54,9 @@ const EditUser = () => { }; useEffect(() => { loadUser().then(); + if (userId) { + fetchGroups().then(); + } }, []); const submit = async () => { @@ -101,13 +117,19 @@ const EditUser = () => { { userId && <> - From 077853416dfda5fa0d1de3fa0a382cfe3ad52f3e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 11:11:19 +0800 Subject: [PATCH 033/410] chore: record ratio detail in log --- controller/relay.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index fb4c358f..7882a09d 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -195,7 +195,9 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if textRequest.MaxTokens != 0 { preConsumedTokens = promptTokens + textRequest.MaxTokens } - ratio := common.GetModelRatio(textRequest.Model) * common.GetGroupRatio(group) + modelRatio := common.GetModelRatio(textRequest.Model) + groupRatio := common.GetGroupRatio(group) + ratio := modelRatio * groupRatio preConsumedQuota := int(float64(preConsumedTokens) * ratio) if consumeQuota { err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota) @@ -258,7 +260,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { common.SysError("Error consuming token remain quota: " + err.Error()) } userId := c.GetInt("id") - model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度", textRequest.Model, quota)) + model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) } }() From cdef10cad86db93780c2c999d5bc358f88a2b62e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 11:11:47 +0800 Subject: [PATCH 034/410] docs: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82628491..4d7400eb 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 5. 支持**令牌管理**,设置令牌的过期时间和使用次数。 6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 7. 支持**通道管理**,批量创建通道。 -8. 支持**用户分组**以及**渠道分组**。 +8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 9. 支持渠道**设置模型列表**。 10. 支持**查看额度明细**。 11. 支持发布公告,设置充值链接,设置新用户初始额度。 From 69153e72315ae14a1039f5cf0ee6dca2feb90831 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 12:37:15 +0800 Subject: [PATCH 035/410] docs: update README --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d7400eb..189bc03a 100644 --- a/README.md +++ b/README.md @@ -200,4 +200,12 @@ https://openai.justsong.cn + 请检查你的令牌额度是否足够,这个和账户额度是分开的。 + 令牌额度仅供用户设置最大使用量,用户可自由设置。 2. 宝塔部署后访问出现空白页面? - + 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。 \ No newline at end of file + + 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。 +3. 提示无可用渠道? + + 请检查的用户分组和渠道分组设置。 + + 以及渠道的模型设置。 + +## 注意 +本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及法律法规的情况下使用,不得用于非法用途。 + +本项目依据 MIT 协议开源,请以某种方式保留 One API 的版权信息。 From 39481eb6c0e2fccd47479acedee7ccedfd265e33 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 11 Jun 2023 16:33:40 +0800 Subject: [PATCH 036/410] chore: add trailing slash for API calling --- web/src/pages/Channel/EditChannel.js | 2 +- web/src/pages/User/EditUser.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index cc40bd98..9bce5d9a 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -61,7 +61,7 @@ const EditChannel = () => { const fetchGroups = async () => { try { - let res = await API.get(`/api/group`); + let res = await API.get(`/api/group/`); setGroupOptions(res.data.data.map((group) => ({ key: group, text: group, diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index 3193dafa..9c9546c5 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -25,7 +25,7 @@ const EditUser = () => { }; const fetchGroups = async () => { try { - let res = await API.get(`/api/group`); + let res = await API.get(`/api/group/`); setGroupOptions(res.data.data.map((group) => ({ key: group, text: group, From 47ca449e327f66a78ade36ddd3a3ddf48f9da6c4 Mon Sep 17 00:00:00 2001 From: quzard <1191890118@qq.com> Date: Sun, 11 Jun 2023 21:04:41 +0800 Subject: [PATCH 037/410] feat: add support for updating balance of channel typpe OpenAI-SB (#146, close #125) * Add support for updating channel balance in OpenAISB * fix: handel error --------- Co-authored-by: JustSong --- controller/channel-billing.go | 86 +++++++++++++++++++---------- web/src/components/ChannelsTable.js | 9 ++- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index de3fc5f9..b1926545 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -37,6 +37,58 @@ type OpenAIUsageResponse struct { TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar } +type OpenAISBUsageResponse struct { + Msg string `json:"msg"` + Data *struct { + Credit string `json:"credit"` + } `json:"data"` +} + +func GetResponseBody(method, url string, channel *model.Channel) ([]byte, error) { + client := &http.Client{} + req, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, err + } + auth := fmt.Sprintf("Bearer %s", channel.Key) + req.Header.Add("Authorization", auth) + res, err := client.Do(req) + if err != nil { + return nil, err + } + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + err = res.Body.Close() + if err != nil { + return nil, err + } + return body, nil +} + +func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { + url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key) + body, err := GetResponseBody("GET", url, channel) + if err != nil { + return 0, err + } + response := OpenAISBUsageResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + return 0, err + } + if response.Data == nil { + return 0, errors.New(response.Msg) + } + balance, err := strconv.ParseFloat(response.Data.Credit, 64) + if err != nil { + return 0, err + } + channel.UpdateBalance(balance) + return balance, nil +} + func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := common.ChannelBaseURLs[channel.Type] switch channel.Type { @@ -48,27 +100,14 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { return 0, errors.New("尚未实现") case common.ChannelTypeCustom: baseURL = channel.BaseURL + case common.ChannelTypeOpenAISB: + return updateChannelOpenAISBBalance(channel) default: return 0, errors.New("尚未实现") } url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) - client := &http.Client{} - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return 0, err - } - auth := fmt.Sprintf("Bearer %s", channel.Key) - req.Header.Add("Authorization", auth) - res, err := client.Do(req) - if err != nil { - return 0, err - } - body, err := io.ReadAll(res.Body) - if err != nil { - return 0, err - } - err = res.Body.Close() + body, err := GetResponseBody("GET", url, channel) if err != nil { return 0, err } @@ -84,20 +123,7 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { startDate = now.AddDate(0, 0, -100).Format("2006-01-02") } url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) - req, err = http.NewRequest("GET", url, nil) - if err != nil { - return 0, err - } - req.Header.Add("Authorization", auth) - res, err = client.Do(req) - if err != nil { - return 0, err - } - body, err = io.ReadAll(res.Body) - if err != nil { - return 0, err - } - err = res.Body.Close() + body, err = GetResponseBody("GET", url, channel) if err != nil { return 0, err } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index be0bba16..999027fc 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -27,6 +27,13 @@ function renderType(type) { return ; } +function renderBalance(type, balance) { + if (type === 5) { + return {balance.toFixed(2)} + } + return ${balance.toFixed(2)} +} + const ChannelsTable = () => { const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); @@ -336,7 +343,7 @@ const ChannelsTable = () => { ${channel.balance.toFixed(2)}} + trigger={renderBalance(channel.type, channel.balance)} basic /> From 955d5f8707f4d0886fe4e26904464e4d6c13e588 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 12 Jun 2023 09:11:48 +0800 Subject: [PATCH 038/410] fix: fix group list not correct (close #147) --- common/group-ratio.go | 1 + common/model-ratio.go | 1 + 2 files changed, 2 insertions(+) diff --git a/common/group-ratio.go b/common/group-ratio.go index 0a9cf4ba..b9efbdad 100644 --- a/common/group-ratio.go +++ b/common/group-ratio.go @@ -17,6 +17,7 @@ func GroupRatio2JSONString() string { } func UpdateGroupRatioByJSONString(jsonStr string) error { + GroupRatio = make(map[string]float64) return json.Unmarshal([]byte(jsonStr), &GroupRatio) } diff --git a/common/model-ratio.go b/common/model-ratio.go index bc7e7be3..fc982491 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -39,6 +39,7 @@ func ModelRatio2JSONString() string { } func UpdateModelRatioByJSONString(jsonStr string) error { + ModelRatio = make(map[string]float64) return json.Unmarshal([]byte(jsonStr), &ModelRatio) } From 8b2ef666efe51f4e455044a1977a74f034a531c0 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 12 Jun 2023 09:40:49 +0800 Subject: [PATCH 039/410] fix: fix OpenAI-SB balance not correct --- web/src/components/ChannelsTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 999027fc..48fd521d 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -29,7 +29,7 @@ function renderType(type) { function renderBalance(type, balance) { if (type === 5) { - return {balance.toFixed(2)} + return ¥{(balance / 10000).toFixed(2)} } return ${balance.toFixed(2)} } From 7c7eb6b7ece0865a94fb563de7376fe7d3e11940 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 12 Jun 2023 16:11:57 +0800 Subject: [PATCH 040/410] fix: now the input field can be array type now (close #149) --- controller/relay-utils.go | 14 ++++++++++++++ controller/relay.go | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/controller/relay-utils.go b/controller/relay-utils.go index bb25fa3b..a2dc2685 100644 --- a/controller/relay-utils.go +++ b/controller/relay-utils.go @@ -58,6 +58,20 @@ func countTokenMessages(messages []Message, model string) int { return tokenNum } +func countTokenInput(input any, model string) int { + switch input.(type) { + case string: + return countTokenText(input.(string), model) + case []string: + text := "" + for _, s := range input.([]string) { + text += s + } + return countTokenText(text, model) + } + return 0 +} + func countTokenText(text string, model string) int { tokenEncoder := getTokenEncoder(model) token := tokenEncoder.Encode(text, nil, nil) diff --git a/controller/relay.go b/controller/relay.go index 7882a09d..35897909 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -38,7 +38,7 @@ type GeneralOpenAIRequest struct { Temperature float64 `json:"temperature"` TopP float64 `json:"top_p"` N int `json:"n"` - Input string `json:"input"` + Input any `json:"input"` } type ChatRequest struct { @@ -189,7 +189,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { case RelayModeCompletions: promptTokens = countTokenText(textRequest.Prompt, textRequest.Model) case RelayModeModeration: - promptTokens = countTokenText(textRequest.Input, textRequest.Model) + promptTokens = countTokenInput(textRequest.Input, textRequest.Model) } preConsumedTokens := common.PreConsumedQuota if textRequest.MaxTokens != 0 { From 0c34ed4c61e1729cd481cae6c545737ec8ce7614 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 13 Jun 2023 17:45:01 +0800 Subject: [PATCH 041/410] docs: update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 189bc03a..aecb8e47 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ sudo certbot --nginx sudo service nginx restart ``` +初始账号用户名为 `root`,密码为 `123456`。 + ### 手动部署 1. 从 [GitHub Releases](https://github.com/songquanpeng/one-api/releases/latest) 下载可执行文件或者从源码编译: ```shell From 323f3d263a44a7fcae8ab8db5af23a95f529bc91 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 09:12:14 +0800 Subject: [PATCH 042/410] feat: add new released models --- common/model-ratio.go | 5 +++++ controller/model.go | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/common/model-ratio.go b/common/model-ratio.go index fc982491..4bdb6fec 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -8,10 +8,15 @@ import "encoding/json" var ModelRatio = map[string]float64{ "gpt-4": 15, "gpt-4-0314": 15, + "gpt-4-0613": 15, "gpt-4-32k": 30, "gpt-4-32k-0314": 30, + "gpt-4-32k-0613": 30, "gpt-3.5-turbo": 1, // $0.002 / 1K tokens "gpt-3.5-turbo-0301": 1, + "gpt-3.5-turbo-0613": 1, + "gpt-3.5-turbo-16k": 2, // $0.004 / 1K tokens + "gpt-3.5-turbo-16k-0613": 2, "text-ada-001": 0.2, "text-babbage-001": 0.25, "text-curie-001": 1, diff --git a/controller/model.go b/controller/model.go index dd3777f7..08819c72 100644 --- a/controller/model.go +++ b/controller/model.go @@ -71,6 +71,33 @@ func init() { Root: "gpt-3.5-turbo-0301", Parent: nil, }, + { + Id: "gpt-3.5-turbo-0613", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "gpt-3.5-turbo-0613", + Parent: nil, + }, + { + Id: "gpt-3.5-turbo-16k", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "gpt-3.5-turbo-16k", + Parent: nil, + }, + { + Id: "gpt-3.5-turbo-16k-0613", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "gpt-3.5-turbo-16k-0613", + Parent: nil, + }, { Id: "gpt-4", Object: "model", @@ -89,6 +116,15 @@ func init() { Root: "gpt-4-0314", Parent: nil, }, + { + Id: "gpt-4-0613", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "gpt-4-0613", + Parent: nil, + }, { Id: "gpt-4-32k", Object: "model", @@ -107,6 +143,15 @@ func init() { Root: "gpt-4-32k-0314", Parent: nil, }, + { + Id: "gpt-4-32k-0613", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "gpt-4-32k-0613", + Parent: nil, + }, { Id: "text-embedding-ada-002", Object: "model", From 38668e73311f0bb85a1080b7dad0565eac56f27e Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 09:41:06 +0800 Subject: [PATCH 043/410] chore: update gpt3.5 completion ratio --- common/model-ratio.go | 12 +++++++----- controller/relay.go | 11 +++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 4bdb6fec..5aa8f2d8 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -2,9 +2,11 @@ package common import "encoding/json" +// ModelRatio // https://platform.openai.com/docs/models/model-endpoint-compatibility // https://openai.com/pricing // TODO: when a new api is enabled, check the pricing here +// 1 === $0.002 / 1K tokens var ModelRatio = map[string]float64{ "gpt-4": 15, "gpt-4-0314": 15, @@ -12,11 +14,11 @@ var ModelRatio = map[string]float64{ "gpt-4-32k": 30, "gpt-4-32k-0314": 30, "gpt-4-32k-0613": 30, - "gpt-3.5-turbo": 1, // $0.002 / 1K tokens - "gpt-3.5-turbo-0301": 1, - "gpt-3.5-turbo-0613": 1, - "gpt-3.5-turbo-16k": 2, // $0.004 / 1K tokens - "gpt-3.5-turbo-16k-0613": 2, + "gpt-3.5-turbo": 0.75, // $0.0015 / 1K tokens + "gpt-3.5-turbo-0301": 0.75, + "gpt-3.5-turbo-0613": 0.75, + "gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens + "gpt-3.5-turbo-16k-0613": 1.5, "text-ada-001": 0.2, "text-babbage-001": 0.25, "text-curie-001": 1, diff --git a/controller/relay.go b/controller/relay.go index 35897909..cf357104 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -239,16 +239,15 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { defer func() { if consumeQuota { quota := 0 - usingGPT4 := strings.HasPrefix(textRequest.Model, "gpt-4") - completionRatio := 1 - if usingGPT4 { + completionRatio := 1.34 // default for gpt-3 + if strings.HasPrefix(textRequest.Model, "gpt-4") { completionRatio = 2 } if isStream { responseTokens := countTokenText(streamResponseText, textRequest.Model) - quota = promptTokens + responseTokens*completionRatio + quota = promptTokens + int(float64(responseTokens)*completionRatio) } else { - quota = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens*completionRatio + quota = textResponse.Usage.PromptTokens + int(float64(textResponse.Usage.CompletionTokens)*completionRatio) } quota = int(float64(quota) * ratio) if ratio != 0 && quota <= 0 { @@ -260,7 +259,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { common.SysError("Error consuming token remain quota: " + err.Error()) } userId := c.GetInt("id") - model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) + model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio, completionRatio)) } }() From 7f9577a3862b4d93eb4bd85badd6928e0c07d66a Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 12:14:08 +0800 Subject: [PATCH 044/410] feat: now one channel can belong to multiple groups (close #153) --- model/ability.go | 15 +++++++++------ web/src/pages/Channel/EditChannel.js | 14 ++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/model/ability.go b/model/ability.go index 26ff65d2..1b33916c 100644 --- a/model/ability.go +++ b/model/ability.go @@ -30,15 +30,18 @@ func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { func (channel *Channel) AddAbilities() error { models_ := strings.Split(channel.Models, ",") + groups_ := strings.Split(channel.Group, ",") abilities := make([]Ability, 0, len(models_)) for _, model := range models_ { - ability := Ability{ - Group: channel.Group, - Model: model, - ChannelId: channel.Id, - Enabled: channel.Status == common.ChannelStatusEnabled, + for _, group := range groups_ { + ability := Ability{ + Group: group, + Model: model, + ChannelId: channel.Id, + Enabled: channel.Status == common.ChannelStatusEnabled, + } + abilities = append(abilities, ability) } - abilities = append(abilities, ability) } return DB.Create(&abilities).Error } diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 9bce5d9a..e25ab2de 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -15,8 +15,8 @@ const EditChannel = () => { key: '', base_url: '', other: '', - group: 'default', models: [], + groups: ['default'] }; const [batch, setBatch] = useState(false); const [inputs, setInputs] = useState(originInputs); @@ -37,6 +37,11 @@ const EditChannel = () => { } else { data.models = data.models.split(",") } + if (data.group === "") { + data.groups = [] + } else { + data.groups = data.group.split(",") + } setInputs(data); } else { showError(message); @@ -94,6 +99,7 @@ const EditChannel = () => { } let res; localInputs.models = localInputs.models.join(",") + localInputs.group = localInputs.groups.join(",") if (isEdit) { res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); } else { @@ -185,14 +191,14 @@ const EditChannel = () => { From d6dbaff3c2704192ef66065d471dcd1223975870 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 12:52:56 +0800 Subject: [PATCH 045/410] fix: fix file not committed --- web/src/helpers/render.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 506de16f..c09a79ad 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -10,10 +10,17 @@ export function renderText(text, limit) { export function renderGroup(group) { if (group === "") { return - } else if (group === "vip" || group === "pro") { - return - } else if (group === "svip" || group === "premium") { - return } - return + let groups = group.split(","); + groups.sort(); + return <> + {groups.map((group) => { + if (group === "vip" || group === "pro") { + return + } else if (group === "svip" || group === "premium") { + return + } + return + })} + } \ No newline at end of file From e09512177a4fe50a98dde247a6023e5f00c3ac12 Mon Sep 17 00:00:00 2001 From: JustSong <39998050+songquanpeng@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:48:31 +0800 Subject: [PATCH 046/410] docs: add issue templates --- .github/ISSUE_TEMPLATE/功能请求.md | 16 ++++++++++++++++ .github/ISSUE_TEMPLATE/报告问题.md | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/功能请求.md create mode 100644 .github/ISSUE_TEMPLATE/报告问题.md diff --git a/.github/ISSUE_TEMPLATE/功能请求.md b/.github/ISSUE_TEMPLATE/功能请求.md new file mode 100644 index 00000000..1708d81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/功能请求.md @@ -0,0 +1,16 @@ +--- +name: 功能请求 +about: 请求加入新功能 +title: '' +labels: '' +assignees: '' + +--- + +**例行检查** ++ [ ] 我已确认目前没有类似 issue ++ [ ] 我已确认我已升级到最新版本 + +**功能描述** + +**应用场景** diff --git a/.github/ISSUE_TEMPLATE/报告问题.md b/.github/ISSUE_TEMPLATE/报告问题.md new file mode 100644 index 00000000..c5b40aa1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/报告问题.md @@ -0,0 +1,20 @@ +--- +name: 报告问题 +about: 使用简练详细的语言描述你的问题 +title: '' +labels: bug +assignees: '' + +--- + +**例行检查** ++ [ ] 我已确认目前没有类似 issue ++ [ ] 我已确认我已升级到最新版本 + +**问题描述** + +**复现步骤** + +**预期结果** + +**相关截图** From 2930577cd63b0ae3bc12aa0f3a8d54b6c0ce64fe Mon Sep 17 00:00:00 2001 From: JustSong <39998050+songquanpeng@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:51:48 +0800 Subject: [PATCH 047/410] docs: update issue template --- .github/ISSUE_TEMPLATE/功能请求.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/功能请求.md b/.github/ISSUE_TEMPLATE/功能请求.md index 1708d81f..101de93a 100644 --- a/.github/ISSUE_TEMPLATE/功能请求.md +++ b/.github/ISSUE_TEMPLATE/功能请求.md @@ -2,7 +2,7 @@ name: 功能请求 about: 请求加入新功能 title: '' -labels: '' +labels: 'enhancement' assignees: '' --- From f426f31bd77fbc7169e0d7dc118f1dea9c32012d Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 14:59:24 +0800 Subject: [PATCH 048/410] docs: update issue template --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ .github/ISSUE_TEMPLATE/功能请求.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..1c746b28 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 项目群聊 + url: https://openai.justsong.cn/ + about: 演示站首页有官方群聊信息 + - name: 付费部署或定制功能 + url: https://openai.justsong.cn/ + about: 加群后联系群主 diff --git a/.github/ISSUE_TEMPLATE/功能请求.md b/.github/ISSUE_TEMPLATE/功能请求.md index 101de93a..223d2feb 100644 --- a/.github/ISSUE_TEMPLATE/功能请求.md +++ b/.github/ISSUE_TEMPLATE/功能请求.md @@ -2,7 +2,7 @@ name: 功能请求 about: 请求加入新功能 title: '' -labels: 'enhancement' +labels: enhancement assignees: '' --- From 54d7a1c2e80d0453286373cc986a09e0107af97a Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 15:02:36 +0800 Subject: [PATCH 049/410] docs: update issue template --- .github/ISSUE_TEMPLATE/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1c746b28..a1202156 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,9 @@ contact_links: - name: 项目群聊 url: https://openai.justsong.cn/ about: 演示站首页有官方群聊信息 + - name: 赞赏支持 + url: https://iamazing.cn/page/reward + about: 请作者喝杯咖啡,以激励作者持续开发 - name: 付费部署或定制功能 url: https://openai.justsong.cn/ about: 加群后联系群主 From f71f01662c04ec9f641426b853a5bc5210841adc Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 15:03:51 +0800 Subject: [PATCH 050/410] docs: update issue template --- .github/ISSUE_TEMPLATE/{报告问题.md => bug_report.md} | 0 .github/ISSUE_TEMPLATE/{功能请求.md => feature_request.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{报告问题.md => bug_report.md} (100%) rename .github/ISSUE_TEMPLATE/{功能请求.md => feature_request.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/报告问题.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/报告问题.md rename to .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/功能请求.md b/.github/ISSUE_TEMPLATE/feature_request.md similarity index 100% rename from .github/ISSUE_TEMPLATE/功能请求.md rename to .github/ISSUE_TEMPLATE/feature_request.md From 07cccdc8c07ee717def97d3094150457044089e8 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 14 Jun 2023 15:13:05 +0800 Subject: [PATCH 051/410] docs: update issue template --- .github/ISSUE_TEMPLATE/bug_report.md | 5 ++++- .github/ISSUE_TEMPLATE/feature_request.md | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c5b40aa1..045004b8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: 报告问题 -about: 使用简练详细的语言描述你的问题 +about: 使用简练详细的语言描述你遇到的问题 title: '' labels: bug assignees: '' @@ -10,6 +10,8 @@ assignees: '' **例行检查** + [ ] 我已确认目前没有类似 issue + [ ] 我已确认我已升级到最新版本 ++ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 ++ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 **问题描述** @@ -18,3 +20,4 @@ assignees: '' **预期结果** **相关截图** +如果没有的话,请删除此节。 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 223d2feb..194ead7c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- name: 功能请求 -about: 请求加入新功能 +about: 使用简练详细的语言描述希望加入的新功能 title: '' labels: enhancement assignees: '' @@ -10,6 +10,8 @@ assignees: '' **例行检查** + [ ] 我已确认目前没有类似 issue + [ ] 我已确认我已升级到最新版本 ++ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 ++ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 **功能描述** From e87ad1f40280e7bbd18ed880bb6d8a1965878dca Mon Sep 17 00:00:00 2001 From: quzard <1191890118@qq.com> Date: Wed, 14 Jun 2023 16:33:03 +0800 Subject: [PATCH 052/410] chore: remove -0613 suffix for Azure (#163) --- controller/relay.go | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/relay.go b/controller/relay.go index cf357104..aeb36da7 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -177,6 +177,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { // https://github.com/songquanpeng/one-api/issues/67 model_ = strings.TrimSuffix(model_, "-0301") model_ = strings.TrimSuffix(model_, "-0314") + model_ = strings.TrimSuffix(model_, "-0613") fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task) } else if channelType == common.ChannelTypePaLM { err := relayPaLM(textRequest, c) From 593e1926e927e84e6db7626f5d084283911cd27f Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 15 Jun 2023 16:32:16 +0800 Subject: [PATCH 053/410] feat: able to disable quota consumption recording (close #156) --- common/constants.go | 2 ++ model/log.go | 3 +++ model/option.go | 3 +++ web/src/components/SystemSetting.js | 8 ++++++++ 4 files changed, 16 insertions(+) diff --git a/common/constants.go b/common/constants.go index 3a8f6d77..23fb3584 100644 --- a/common/constants.go +++ b/common/constants.go @@ -35,6 +35,8 @@ var WeChatAuthEnabled = false var TurnstileCheckEnabled = false var RegisterEnabled = true +var LogConsumeEnabled = true + var SMTPServer = "" var SMTPPort = 587 var SMTPAccount = "" diff --git a/model/log.go b/model/log.go index 52d02a6f..a21cb6b4 100644 --- a/model/log.go +++ b/model/log.go @@ -22,6 +22,9 @@ const ( ) func RecordLog(userId int, logType int, content string) { + if logType == LogTypeConsume && !common.LogConsumeEnabled { + return + } log := &Log{ UserId: userId, CreatedAt: common.GetTimestamp(), diff --git a/model/option.go b/model/option.go index 5e74984d..101f694d 100644 --- a/model/option.go +++ b/model/option.go @@ -34,6 +34,7 @@ func InitOptionMap() { common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled) + common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled) common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64) common.OptionMap["SMTPServer"] = "" common.OptionMap["SMTPFrom"] = "" @@ -134,6 +135,8 @@ func updateOptionMap(key string, value string) (err error) { common.RegisterEnabled = boolValue case "AutomaticDisableChannelEnabled": common.AutomaticDisableChannelEnabled = boolValue + case "LogConsumeEnabled": + common.LogConsumeEnabled = boolValue } } switch key { diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 3b40822b..ac6d9263 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -34,6 +34,7 @@ const SystemSetting = () => { TopUpLink: '', AutomaticDisableChannelEnabled: '', ChannelDisableThreshold: 0, + LogConsumeEnabled: '', }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); @@ -68,6 +69,7 @@ const SystemSetting = () => { case 'TurnstileCheckEnabled': case 'RegisterEnabled': case 'AutomaticDisableChannelEnabled': + case 'LogConsumeEnabled': value = inputs[key] === 'true' ? 'false' : 'true'; break; default: @@ -349,6 +351,12 @@ const SystemSetting = () => { placeholder='为一个 JSON 文本,键为分组名称,值为倍率' /> + 保存运营设置
From 538a5d7a9b9abf5af066368860888644ff0e5033 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 15 Jun 2023 21:51:30 +0800 Subject: [PATCH 054/410] docs: update issue template (#164) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a1202156..d631ee48 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,7 @@ blank_issues_enabled: false contact_links: - name: 项目群聊 url: https://openai.justsong.cn/ - about: 演示站首页有官方群聊信息 + about: QQ 群:828520184,自动审核,备注 One API - name: 赞赏支持 url: https://iamazing.cn/page/reward about: 请作者喝杯咖啡,以激励作者持续开发 From 5f23f59d1c38b2ff62256b5ea923a30e455b9bc1 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 16 Jun 2023 00:24:38 +0800 Subject: [PATCH 055/410] docs: update notice --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aecb8e47..821f0a41 100644 --- a/README.md +++ b/README.md @@ -210,4 +210,6 @@ https://openai.justsong.cn ## 注意 本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及法律法规的情况下使用,不得用于非法用途。 -本项目依据 MIT 协议开源,请以某种方式保留 One API 的版权信息。 +本项目使用 MIT 协议进行开源,请以某种方式保留 One API 的版权信息。 + +依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。 \ No newline at end of file From 630156dc0a937396e81a3be92ae64a73f235240e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=9F=8E=E9=93=AD?= Date: Fri, 16 Jun 2023 14:20:25 +0800 Subject: [PATCH 056/410] fix: the prompt field can be array type now (close #166, #167) * fix: the prompt field can be array type now (close #166) * fix: fix prompt type --------- Co-authored-by: JustSong --- controller/relay.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index aeb36da7..3462409f 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -32,7 +32,7 @@ const ( type GeneralOpenAIRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` - Prompt string `json:"prompt"` + Prompt any `json:"prompt"` Stream bool `json:"stream"` MaxTokens int `json:"max_tokens"` Temperature float64 `json:"temperature"` @@ -188,7 +188,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { case RelayModeChatCompletions: promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model) case RelayModeCompletions: - promptTokens = countTokenText(textRequest.Prompt, textRequest.Model) + promptTokens = countTokenInput(textRequest.Prompt, textRequest.Model) case RelayModeModeration: promptTokens = countTokenInput(textRequest.Input, textRequest.Model) } From 58fb18aaceccfdcbaf7aa9d247958a382f4c6f47 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 16 Jun 2023 14:24:16 +0800 Subject: [PATCH 057/410] fix: do not record completion ratio anymore --- controller/relay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/relay.go b/controller/relay.go index 3462409f..2033476f 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -260,7 +260,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { common.SysError("Error consuming token remain quota: " + err.Error()) } userId := c.GetInt("id") - model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio, completionRatio)) + model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) } }() From 760183a97026a9b40aa4288da702c016bb554e9d Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 16 Jun 2023 15:20:06 +0800 Subject: [PATCH 058/410] feat: record used quota & request count (close #102, #165) --- controller/relay.go | 1 + model/user.go | 14 ++++++++++++++ web/src/components/UsersTable.js | 10 +++++++--- web/src/helpers/render.js | 30 +++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index 2033476f..bb519258 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -261,6 +261,7 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } userId := c.GetInt("id") model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) + model.UpdateUserUsedQuotaAndRequestCount(userId, quota) } }() diff --git a/model/user.go b/model/user.go index 60278a07..a8fb7842 100644 --- a/model/user.go +++ b/model/user.go @@ -23,6 +23,8 @@ type User struct { VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management Quota int `json:"quota" gorm:"type:int;default:0"` + UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota + RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number Group string `json:"group" gorm:"type:varchar(32);default:'default'"` } @@ -262,3 +264,15 @@ func GetRootUserEmail() (email string) { DB.Model(&User{}).Where("role = ?", common.RoleRootUser).Select("email").Find(&email) return email } + +func UpdateUserUsedQuotaAndRequestCount(id int, quota int) { + err := DB.Model(&User{}).Where("id = ?", id).Updates( + map[string]interface{}{ + "used_quota": gorm.Expr("used_quota + ?", quota), + "request_count": gorm.Expr("request_count + ?", 1), + }, + ).Error + if err != nil { + common.SysError("Failed to update user used quota and request count: " + err.Error()) + } +} diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index f2e8521e..d64c69af 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import { API, showError, showSuccess } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; -import { renderGroup, renderText } from '../helpers/render'; +import { renderGroup, renderNumber, renderText } from '../helpers/render'; function renderRole(role) { switch (role) { @@ -197,7 +197,7 @@ const UsersTable = () => { sortUser('quota'); }} > - 剩余额度 + 统计信息 { {renderGroup(user.group)} {user.email ? renderText(user.email, 30) : '无'} - {user.quota} + + {renderNumber(user.quota)}} /> + {renderNumber(user.used_quota)}} /> + {renderNumber(user.request_count)}} /> + {renderRole(user.role)} {renderStatus(user.status)} diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index c09a79ad..11096194 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -8,19 +8,31 @@ export function renderText(text, limit) { } export function renderGroup(group) { - if (group === "") { - return + if (group === '') { + return ; } - let groups = group.split(","); + let groups = group.split(','); groups.sort(); return <> {groups.map((group) => { - if (group === "vip" || group === "pro") { - return - } else if (group === "svip" || group === "premium") { - return + if (group === 'vip' || group === 'pro') { + return ; + } else if (group === 'svip' || group === 'premium') { + return ; } - return + return ; })} - + ; +} + +export function renderNumber(num) { + if (num >= 1000000000) { + return (num / 1000000000).toFixed(1) + 'B'; + } else if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 10000) { + return (num / 1000).toFixed(1) + 'k'; + } else { + return num; + } } \ No newline at end of file From 0cdab80a6e05441d265646d297f22d22da4a4aeb Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 16 Jun 2023 16:02:00 +0800 Subject: [PATCH 059/410] feat: record channel's used quota (close #137) --- controller/relay.go | 2 ++ model/channel.go | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/controller/relay.go b/controller/relay.go index bb519258..8e7073f7 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -262,6 +262,8 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { userId := c.GetInt("id") model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) model.UpdateUserUsedQuotaAndRequestCount(userId, quota) + channelId := c.GetInt("channel_id") + model.UpdateChannelUsedQuota(channelId, quota) } }() diff --git a/model/channel.go b/model/channel.go index fdc89eb9..e53efc20 100644 --- a/model/channel.go +++ b/model/channel.go @@ -1,6 +1,7 @@ package model import ( + "gorm.io/gorm" "one-api/common" ) @@ -20,6 +21,7 @@ type Channel struct { BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"` Models string `json:"models"` Group string `json:"group" gorm:"type:varchar(32);default:'default'"` + UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` } func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { @@ -136,3 +138,10 @@ func UpdateChannelStatusById(id int, status int) { common.SysError("failed to update channel status: " + err.Error()) } } + +func UpdateChannelUsedQuota(id int, quota int) { + err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error + if err != nil { + common.SysError("failed to update channel used quota: " + err.Error()) + } +} From 549e944b95a1ac0aba088d8fa5d8dedbe4c3b8cc Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 16 Jun 2023 16:40:07 +0800 Subject: [PATCH 060/410] docs: update README --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 821f0a41..1c055b88 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 · 部署教程 · + 使用方法 + · 意见反馈 · 截图展示 @@ -158,11 +160,24 @@ sudo service nginx restart 等到系统启动后,使用 `root` 用户登录系统并做进一步的配置。 -## 使用方式 -在`渠道`页面中添加你的 API Key,之后在`令牌`页面中新增一个访问令牌。 +## 使用方法 +在`渠道`页面中添加你的 API Key,之后在`令牌`页面中新增访问令牌。 之后就可以使用你的令牌访问 One API 了,使用方式与 [OpenAI API](https://platform.openai.com/docs/api-reference/introduction) 一致。 +你需要在各种用到 OpenAI API 的地方设置 API Base 为你的 One API 的部署地址,例如:`https://openai.justsong.cn`,API Key 则为你在 One API 中生成的令牌。 + +注意,具体的 API Base 的格式取决于你所使用的客户端。 + +```mermaid +graph LR + A(用户) + A --->|请求| B(One API) + B -->|中继请求| C(OpenAI) + B -->|中继请求| D(Azure) + B -->|中继请求| E(其他下游渠道) +``` + 可以通过在令牌后面添加渠道 ID 的方式指定使用哪一个渠道处理本次请求,例如:`Authorization: Bearer ONE_API_KEY-CHANNEL_ID`。 注意,需要是管理员用户创建的令牌才能指定渠道 ID。 From 57b213a0359bb2bd29755219e3d8241b03f7ffd2 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 16 Jun 2023 16:40:54 +0800 Subject: [PATCH 061/410] docs: update README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1c055b88..8eeb0673 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,6 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用

- 程序下载 - · 部署教程 · 使用方法 From 70ed126ccb8a58fa00c73544198180c1354e1872 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 09:46:07 +0800 Subject: [PATCH 062/410] feat: return a not found response if requested a wrong API endpoints --- controller/relay.go | 12 ++++++++++++ router/web-router.go | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/controller/relay.go b/controller/relay.go index 8e7073f7..cf01f180 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -398,3 +398,15 @@ func RelayNotImplemented(c *gin.Context) { "error": err, }) } + +func RelayNotFound(c *gin.Context) { + err := OpenAIError{ + Message: fmt.Sprintf("API not found: %s:%s", c.Request.Method, c.Request.URL.Path), + Type: "one_api_error", + Param: "", + Code: "api_not_found", + } + c.JSON(http.StatusOK, gin.H{ + "error": err, + }) +} diff --git a/router/web-router.go b/router/web-router.go index 8f6d1ac4..19fc0c04 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -7,7 +7,9 @@ import ( "github.com/gin-gonic/gin" "net/http" "one-api/common" + "one-api/controller" "one-api/middleware" + "strings" ) func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { @@ -16,6 +18,10 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { router.Use(middleware.Cache()) router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) router.NoRoute(func(c *gin.Context) { + if strings.HasPrefix(c.Request.RequestURI, "/v1") { + controller.RelayNotFound(c) + return + } c.Header("Cache-Control", "no-cache") c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) }) From b7d71b4f0aef9e9f68c0e3a2dd709bd0ec0c2f67 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 17 Jun 2023 10:08:04 +0800 Subject: [PATCH 063/410] feat: support update AIProxy balance (#171) * Add: support update AIProxy balance * fix auth header * chore: update balance renderer --------- Co-authored-by: JustSong --- controller/channel-billing.go | 54 +++++++++++++++++++++++++---- web/src/components/ChannelsTable.js | 18 +++++++--- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index b1926545..1ff7ff42 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -4,13 +4,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" "one-api/common" "one-api/model" "strconv" "time" + + "github.com/gin-gonic/gin" ) // https://github.com/songquanpeng/one-api/issues/79 @@ -44,14 +45,31 @@ type OpenAISBUsageResponse struct { } `json:"data"` } -func GetResponseBody(method, url string, channel *model.Channel) ([]byte, error) { +type AIProxyUserOverviewResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + ErrorCode int `json:"error_code"` + Data struct { + TotalPoints float64 `json:"totalPoints"` + } `json:"data"` +} + +// GetAuthHeader get auth header +func GetAuthHeader(token string) http.Header { + h := http.Header{} + h.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + return h +} + +func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) { client := &http.Client{} req, err := http.NewRequest(method, url, nil) if err != nil { return nil, err } - auth := fmt.Sprintf("Bearer %s", channel.Key) - req.Header.Add("Authorization", auth) + for k := range headers { + req.Header.Add(k, headers.Get(k)) + } res, err := client.Do(req) if err != nil { return nil, err @@ -69,7 +87,7 @@ func GetResponseBody(method, url string, channel *model.Channel) ([]byte, error) func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key) - body, err := GetResponseBody("GET", url, channel) + body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) if err != nil { return 0, err } @@ -89,6 +107,26 @@ func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { return balance, nil } +func updateChannelAIProxyBalance(channel *model.Channel) (float64, error) { + url := "https://aiproxy.io/api/report/getUserOverview" + headers := http.Header{} + headers.Add("Api-Key", channel.Key) + body, err := GetResponseBody("GET", url, channel, headers) + if err != nil { + return 0, err + } + response := AIProxyUserOverviewResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + return 0, err + } + if !response.Success { + return 0, fmt.Errorf("code: %d, message: %s", response.ErrorCode, response.Message) + } + channel.UpdateBalance(response.Data.TotalPoints) + return response.Data.TotalPoints, nil +} + func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := common.ChannelBaseURLs[channel.Type] switch channel.Type { @@ -102,12 +140,14 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL = channel.BaseURL case common.ChannelTypeOpenAISB: return updateChannelOpenAISBBalance(channel) + case common.ChannelTypeAIProxy: + return updateChannelAIProxyBalance(channel) default: return 0, errors.New("尚未实现") } url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) - body, err := GetResponseBody("GET", url, channel) + body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) if err != nil { return 0, err } @@ -123,7 +163,7 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { startDate = now.AddDate(0, 0, -100).Format("2006-01-02") } url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) - body, err = GetResponseBody("GET", url, channel) + body, err = GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) if err != nil { return 0, err } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 48fd521d..f5f25ae9 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; -import { renderGroup } from '../helpers/render'; +import { renderGroup, renderNumber } from '../helpers/render'; function renderTimestamp(timestamp) { return ( @@ -28,10 +28,17 @@ function renderType(type) { } function renderBalance(type, balance) { - if (type === 5) { - return ¥{(balance / 10000).toFixed(2)} + switch (type) { + case 1: // OpenAI + case 8: // 自定义 + return ${balance.toFixed(2)}; + case 5: // OpenAI-SB + return ¥{(balance / 10000).toFixed(2)}; + case 10: // AI Proxy + return {renderNumber(balance)}; + default: + return 不支持; } - return ${balance.toFixed(2)} } const ChannelsTable = () => { @@ -422,7 +429,8 @@ const ChannelsTable = () => { - + Date: Sat, 17 Jun 2023 10:19:13 +0800 Subject: [PATCH 064/410] docs: deploy to Zeabur (#170) * docs: deploy to Zeabur * docs: deploy to Zeabur * docs: deploy to Zeabur * docs: update README --------- Co-authored-by: JustSong --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 8eeb0673..1d5539fe 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,27 @@ sudo service nginx restart 环境变量的具体使用方法详见[此处](#环境变量)。 + +### 部署到第三方平台 +

+部署到 Zeabur +
+ +> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。 + +1. 首先 fork 一份代码。 +2. 进入 [Zeabur](https://zeabur.com/),登录,进入控制台。 +3. 新建一个 Project,在 Service -> Add Service 选择 Marketplace,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。 +4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。 +5. 然后在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。 +6. Deploy 会自动开始,先取消。进入下方 Variable,添加一个 `PORT`,值为 `3000`,再添加一个 `SQL_DSN`,值为 `:@tcp(:)/one-api` ,然后保存。 注意如果不填写 `SQL_DSN`,数据将无法持久化,重新部署后数据会丢失。 +7. 选择 Redeploy。 +8. 进入下方 Domains,选择一个合适的域名前缀,如 "my-one-api",最终域名为 "my-one-api.zeabur.app",也可以 CNAME 自己的域名。 +9. 等待部署完成,点击生成的域名进入 One API。 + +
+
+ ## 配置 系统本身开箱即用。 From d79289ccdd1f49bc2e66b756bdd968ffc6615a4e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 11:03:01 +0800 Subject: [PATCH 065/410] fix: fix footer not updated asap --- web/src/components/Footer.js | 24 ++++++++++++++++++++++-- web/src/components/OtherSetting.js | 13 +++++++------ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js index 24aaddfe..334ee379 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/Footer.js @@ -1,11 +1,31 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Container, Segment } from 'semantic-ui-react'; import { getFooterHTML, getSystemName } from '../helpers'; const Footer = () => { const systemName = getSystemName(); - const footer = getFooterHTML(); + const [footer, setFooter] = useState(getFooterHTML()); + let remainCheckTimes = 5; + + const loadFooter = () => { + let footer_html = localStorage.getItem('footer_html'); + if (footer_html) { + setFooter(footer_html); + } + }; + + useEffect(() => { + const timer = setInterval(() => { + if (remainCheckTimes <= 0) { + clearInterval(timer); + return; + } + remainCheckTimes--; + loadFooter(); + }, 200); + return () => clearTimeout(timer); + }, []); return ( diff --git a/web/src/components/OtherSetting.js b/web/src/components/OtherSetting.js index 1ea5bf8f..bfe36d6b 100644 --- a/web/src/components/OtherSetting.js +++ b/web/src/components/OtherSetting.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react'; +import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react'; import { API, showError, showSuccess } from '../helpers'; import { marked } from 'marked'; @@ -10,13 +10,13 @@ const OtherSetting = () => { About: '', SystemName: '', Logo: '', - HomePageContent: '', + HomePageContent: '' }); let [loading, setLoading] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false); const [updateData, setUpdateData] = useState({ tag_name: '', - content: '', + content: '' }); const getOptions = async () => { @@ -43,7 +43,7 @@ const OtherSetting = () => { setLoading(true); const res = await API.put('/api/option/', { key, - value, + value }); const { success, message } = res.data; if (success) { @@ -97,7 +97,7 @@ const OtherSetting = () => { } else { setUpdateData({ tag_name: tag_name, - content: marked.parse(body), + content: marked.parse(body) }); setShowUpdateModal(true); } @@ -153,7 +153,7 @@ const OtherSetting = () => { style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} /> - submitOption('HomePageContent')}>保存首页内容 + submitOption('HomePageContent')}>保存首页内容 { /> 保存关于 + 移除 One API 的版权标识必须首先获得授权,后续版本将通过授权码强制执行。 Date: Sat, 17 Jun 2023 11:24:32 +0800 Subject: [PATCH 066/410] docs: update README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d5539fe..4ce1eec7 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,12 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 1. 支持自定义系统名称,logo 以及页脚。 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 13. 支持通过系统访问令牌访问管理 API。 -14. 支持用户管理,支持**多种用户登录注册方式**: +14. 支持 Cloudflare Turnstile 用户校验。 +15. 支持用户管理,支持**多种用户登录注册方式**: + 邮箱登录注册以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 -15. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 +16. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 ## 部署 ### 基于 Docker 进行部署 From 21111126a298055b9f60c37f75194e4b64bb7a55 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 11:30:31 +0800 Subject: [PATCH 067/410] docs: update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ce1eec7..f3576165 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,10 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] [API2D](https://api2d.com/r/197971) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`) - + [x] [AI.LS](https://ai.ls) - + [x] [OpenAI Max](https://openaimax.com) + [x] [OpenAI-SB](https://openai-sb.com) + [x] [CloseAI](https://console.openai-asia.com/r/2412) + + [x] [AI.LS](https://ai.ls) + + [x] [OpenAI Max](https://openaimax.com) + [x] 自定义渠道:例如各种未收录的第三方代理服务 2. 支持通过**负载均衡**的方式访问多个渠道。 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 From d754620ef7eb98855456d2d8103766ed20ebf1e6 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 12:07:58 +0800 Subject: [PATCH 068/410] docs: update README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3576165..746cc52b 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 ## 部署 ### 基于 Docker 进行部署 -执行:`docker run -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api` +部署命令:`docker run -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api` + +更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` `-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。 @@ -208,7 +210,7 @@ graph LR + 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` 2. `SESSION_SECRET`:设置之后将使用固定的会话密钥,这样系统重新启动后已登录用户的 cookie 将依旧有效。 + 例子:`SESSION_SECRET=random_string` -3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite。 +3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 8.0 版本。 + 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/one-api` 4. `FRONTEND_BASE_URL`:设置之后将使用指定的前端地址,而非后端地址。 + 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn` From 6dcffca06521f03d97e3002616fe51e5195a2fd8 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 12:55:48 +0800 Subject: [PATCH 069/410] docs: update README --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 746cc52b..495039b8 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 ## 部署 ### 基于 Docker 进行部署 -部署命令:`docker run -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api` +部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -v /home/ubuntu/data/one-api:/data justsong/one-api` 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` @@ -154,6 +154,26 @@ sudo service nginx restart 环境变量的具体使用方法详见[此处](#环境变量)。 +### 部署第三方服务配合 One API 使用 +> 欢迎 PR 添加更多示例。 + +#### ChatGPT Next Web +项目主页:https://github.com/Yidadaa/ChatGPT-Next-Web + +```bash +docker run --name chat-next-web -d -p 3001:3000 -e BASE_URL=https://openai.justsong.cn yidadaa/chatgpt-next-web +``` + +注意修改端口号和 `BASE_URL`。 + +#### ChatGPT Web +项目主页:https://github.com/Chanzhaoyu/chatgpt-web + +```bash +docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://openai.justsong.cn -e OPENAI_API_KEY=sk-xxx chenzhaoyu94/chatgpt-web +``` + +注意修改端口号、`OPENAI_API_BASE_URL` 和 `OPENAI_API_KEY`。 ### 部署到第三方平台
From 46c43396d8f8c23ad2c3f2f5a669475ea7a3794c Mon Sep 17 00:00:00 2001 From: Miniers Date: Sat, 17 Jun 2023 14:56:03 +0800 Subject: [PATCH 070/410] feat: add token name to log (#172) * add token name to log * chore: update expression --------- Co-authored-by: JustSong --- controller/relay.go | 3 ++- middleware/auth.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/controller/relay.go b/controller/relay.go index cf01f180..6d957a0a 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -259,8 +259,9 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { common.SysError("Error consuming token remain quota: " + err.Error()) } + tokenName := c.GetString("token_name") userId := c.GetInt("id") - model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", textRequest.Model, quota, modelRatio, groupRatio)) + model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", tokenName, textRequest.Model, quota, modelRatio, groupRatio)) model.UpdateUserUsedQuotaAndRequestCount(userId, quota) channelId := c.GetInt("channel_id") model.UpdateChannelUsedQuota(channelId, quota) diff --git a/middleware/auth.go b/middleware/auth.go index f652f058..c172078c 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -112,6 +112,7 @@ func TokenAuth() func(c *gin.Context) { } c.Set("id", token.UserId) c.Set("token_id", token.Id) + c.Set("token_name", token.Name) requestURL := c.Request.URL.String() consumeQuota := true if strings.HasPrefix(requestURL, "/v1/models") { From a43b1e2add1c8ae417a84b19780b8195e788d9f9 Mon Sep 17 00:00:00 2001 From: Kidultx Date: Sat, 17 Jun 2023 15:20:51 +0800 Subject: [PATCH 071/410] feat: support API2GPT platform (#173) * support API2GPT platform * chore: update balance renderer --------- Co-authored-by: JustSong --- README.md | 1 + common/constants.go | 5 ++++- controller/channel-billing.go | 26 ++++++++++++++++++++++++++ web/src/components/ChannelsTable.js | 2 ++ web/src/constants/channel.constants.js | 3 ++- 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 495039b8..ba331947 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`) + [x] [OpenAI-SB](https://openai-sb.com) + + [x] [API2GPT](http://console.api2gpt.com/m/00002S) + [x] [CloseAI](https://console.openai-asia.com/r/2412) + [x] [AI.LS](https://ai.ls) + [x] [OpenAI Max](https://openaimax.com) diff --git a/common/constants.go b/common/constants.go index 23fb3584..f7467d68 100644 --- a/common/constants.go +++ b/common/constants.go @@ -1,9 +1,10 @@ package common import ( - "github.com/google/uuid" "sync" "time" + + "github.com/google/uuid" ) var StartTime = time.Now().Unix() // unit: second @@ -133,6 +134,7 @@ const ( ChannelTypeAILS = 9 ChannelTypeAIProxy = 10 ChannelTypePaLM = 11 + ChannelTypeAPI2GPT = 12 ) var ChannelBaseURLs = []string{ @@ -148,4 +150,5 @@ var ChannelBaseURLs = []string{ "https://api.caipacity.com", // 9 "https://api.aiproxy.io", // 10 "", // 11 + "https://api.api2gpt.com", // 12 } diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 1ff7ff42..c9c52475 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -54,6 +54,13 @@ type AIProxyUserOverviewResponse struct { } `json:"data"` } +type API2GPTUsageResponse struct { + Object string `json:"object"` + TotalGranted float64 `json:"total_granted"` + TotalUsed float64 `json:"total_used"` + TotalRemaining float64 `json:"total_remaining"` +} + // GetAuthHeader get auth header func GetAuthHeader(token string) http.Header { h := http.Header{} @@ -127,6 +134,23 @@ func updateChannelAIProxyBalance(channel *model.Channel) (float64, error) { return response.Data.TotalPoints, nil } +func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) { + url := "https://api.api2gpt.com/dashboard/billing/credit_grants" + body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) + + if err != nil { + return 0, err + } + response := API2GPTUsageResponse{} + err = json.Unmarshal(body, &response) + fmt.Print(response) + if err != nil { + return 0, err + } + channel.UpdateBalance(response.TotalRemaining) + return response.TotalRemaining, nil +} + func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := common.ChannelBaseURLs[channel.Type] switch channel.Type { @@ -142,6 +166,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { return updateChannelOpenAISBBalance(channel) case common.ChannelTypeAIProxy: return updateChannelAIProxyBalance(channel) + case common.ChannelTypeAPI2GPT: + return updateChannelAPI2GPTBalance(channel) default: return 0, errors.New("尚未实现") } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index f5f25ae9..7bc683ea 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -36,6 +36,8 @@ function renderBalance(type, balance) { return ¥{(balance / 10000).toFixed(2)}; case 10: // AI Proxy return {renderNumber(balance)}; + case 12: // API2GPT + return ¥{balance.toFixed(2)}; default: return 不支持; } diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index f84a4deb..6498f033 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -8,5 +8,6 @@ export const CHANNEL_OPTIONS = [ { key: 6, text: 'OpenAI Max', value: 6, color: 'violet' }, { key: 7, text: 'OhMyGPT', value: 7, color: 'purple' }, { key: 9, text: 'AI.LS', value: 9, color: 'yellow' }, - { key: 10, text: 'AI Proxy', value: 10, color: 'purple' } + { key: 10, text: 'AI Proxy', value: 10, color: 'purple' }, + { key: 12, text: 'API2GPT', value: 12, color: 'blue' } ]; From 6855d0dc3916fcc8cf7b888df199a82345f8da6c Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 15:30:14 +0800 Subject: [PATCH 072/410] chore: add x-requested-with header in CORS setting --- middleware/cors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/cors.go b/middleware/cors.go index 0197ef02..d5a5ed71 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -10,6 +10,6 @@ func CORS() gin.HandlerFunc { config.AllowAllOrigins = true config.AllowCredentials = true config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} - config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept", "Connection"} + config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "Accept", "Connection", "x-requested-with"} return cors.New(config) } From a909972313684274b1cb9937b70a54376e0e7744 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 15:44:04 +0800 Subject: [PATCH 073/410] fix: limit the length of email --- web/src/components/UsersTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index d64c69af..adb7d20b 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -240,7 +240,7 @@ const UsersTable = () => { /> {renderGroup(user.group)} - {user.email ? renderText(user.email, 30) : '无'} + {user.email ? renderText(user.email, 20) : '无'} {renderNumber(user.quota)}} /> {renderNumber(user.used_quota)}} /> From eb70b84665ccc9ab140651a64e76530a96e701ac Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 15:54:14 +0800 Subject: [PATCH 074/410] chore: update api endpoint for CloseAI --- README.md | 2 +- common/constants.go | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ba331947..49e9f421 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`) + [x] [OpenAI-SB](https://openai-sb.com) + [x] [API2GPT](http://console.api2gpt.com/m/00002S) - + [x] [CloseAI](https://console.openai-asia.com/r/2412) + + [x] [CloseAI](https://console.closeai-asia.com/r/2412) + [x] [AI.LS](https://ai.ls) + [x] [OpenAI Max](https://openaimax.com) + [x] 自定义渠道:例如各种未收录的第三方代理服务 diff --git a/common/constants.go b/common/constants.go index f7467d68..a6575ca6 100644 --- a/common/constants.go +++ b/common/constants.go @@ -138,17 +138,17 @@ const ( ) var ChannelBaseURLs = []string{ - "", // 0 - "https://api.openai.com", // 1 - "https://oa.api2d.net", // 2 - "", // 3 - "https://api.openai-asia.com", // 4 - "https://api.openai-sb.com", // 5 - "https://api.openaimax.com", // 6 - "https://api.ohmygpt.com", // 7 - "", // 8 - "https://api.caipacity.com", // 9 - "https://api.aiproxy.io", // 10 - "", // 11 - "https://api.api2gpt.com", // 12 + "", // 0 + "https://api.openai.com", // 1 + "https://oa.api2d.net", // 2 + "", // 3 + "https://api.openai-proxy.org", // 4 + "https://api.openai-sb.com", // 5 + "https://api.openaimax.com", // 6 + "https://api.ohmygpt.com", // 7 + "", // 8 + "https://api.caipacity.com", // 9 + "https://api.aiproxy.io", // 10 + "", // 11 + "https://api.api2gpt.com", // 12 } From c5837c3bb735dc5747987d2bff135a0e318af866 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 18:12:58 +0800 Subject: [PATCH 075/410] feat: support aff now (close #75) --- common/constants.go | 2 ++ common/utils.go | 9 +++++++ controller/github.go | 2 +- controller/user.go | 35 +++++++++++++++++++++++++-- controller/wechat.go | 2 +- model/option.go | 6 +++++ model/user.go | 24 +++++++++++++++++- router/api-router.go | 1 + web/src/components/PersonalSetting.js | 15 +++++++++++- web/src/components/RegisterForm.js | 8 ++++++ web/src/components/SystemSetting.js | 34 +++++++++++++++++++++++++- 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/common/constants.go b/common/constants.go index a6575ca6..471f6ff2 100644 --- a/common/constants.go +++ b/common/constants.go @@ -55,6 +55,8 @@ var TurnstileSiteKey = "" var TurnstileSecretKey = "" var QuotaForNewUser = 0 +var QuotaForInviter = 0 +var QuotaForInvitee = 0 var ChannelDisableThreshold = 5.0 var AutomaticDisableChannelEnabled = false var QuotaRemindThreshold = 1000 diff --git a/common/utils.go b/common/utils.go index de15ce17..1329c1a0 100644 --- a/common/utils.go +++ b/common/utils.go @@ -157,6 +157,15 @@ func GenerateKey() string { return string(key) } +func GetRandomString(length int) string { + rand.Seed(time.Now().UnixNano()) + key := make([]byte, length) + for i := 0; i < length; i++ { + key[i] = keyChars[rand.Intn(len(keyChars))] + } + return string(key) +} + func GetTimestamp() int64 { return time.Now().Unix() } diff --git a/controller/github.go b/controller/github.go index 93c2e8d3..e1c64130 100644 --- a/controller/github.go +++ b/controller/github.go @@ -125,7 +125,7 @@ func GitHubOAuth(c *gin.Context) { user.Role = common.RoleCommonUser user.Status = common.UserStatusEnabled - if err := user.Insert(); err != nil { + if err := user.Insert(0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), diff --git a/controller/user.go b/controller/user.go index 09eaccd1..89e32096 100644 --- a/controller/user.go +++ b/controller/user.go @@ -150,15 +150,18 @@ func Register(c *gin.Context) { return } } + affCode := user.AffCode // this code is the inviter's code, not the user's own code + inviterId, _ := model.GetUserIdByAffCode(affCode) cleanUser := model.User{ Username: user.Username, Password: user.Password, DisplayName: user.Username, + InviterId: inviterId, } if common.EmailVerificationEnabled { cleanUser.Email = user.Email } - if err := cleanUser.Insert(); err != nil { + if err := cleanUser.Insert(inviterId); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), @@ -280,6 +283,34 @@ func GenerateAccessToken(c *gin.Context) { return } +func GetAffCode(c *gin.Context) { + id := c.GetInt("id") + user, err := model.GetUserById(id, true) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + if user.AffCode == "" { + user.AffCode = common.GetRandomString(4) + if err := user.Update(false); err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": user.AffCode, + }) + return +} + func GetSelf(c *gin.Context) { id := c.GetInt("id") user, err := model.GetUserById(id, false) @@ -495,7 +526,7 @@ func CreateUser(c *gin.Context) { Password: user.Password, DisplayName: user.DisplayName, } - if err := cleanUser.Insert(); err != nil { + if err := cleanUser.Insert(0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), diff --git a/controller/wechat.go b/controller/wechat.go index 5620e8d3..ff4c9fb6 100644 --- a/controller/wechat.go +++ b/controller/wechat.go @@ -85,7 +85,7 @@ func WeChatAuth(c *gin.Context) { user.Role = common.RoleCommonUser user.Status = common.UserStatusEnabled - if err := user.Insert(); err != nil { + if err := user.Insert(0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), diff --git a/model/option.go b/model/option.go index 101f694d..32d655ac 100644 --- a/model/option.go +++ b/model/option.go @@ -56,6 +56,8 @@ func InitOptionMap() { common.OptionMap["TurnstileSiteKey"] = "" common.OptionMap["TurnstileSecretKey"] = "" common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) + common.OptionMap["QuotaForInviter"] = strconv.Itoa(common.QuotaForInviter) + common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee) common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold) common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota) common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() @@ -175,6 +177,10 @@ func updateOptionMap(key string, value string) (err error) { common.TurnstileSecretKey = value case "QuotaForNewUser": common.QuotaForNewUser, _ = strconv.Atoi(value) + case "QuotaForInviter": + common.QuotaForInviter, _ = strconv.Atoi(value) + case "QuotaForInvitee": + common.QuotaForInvitee, _ = strconv.Atoi(value) case "QuotaRemindThreshold": common.QuotaRemindThreshold, _ = strconv.Atoi(value) case "PreConsumedQuota": diff --git a/model/user.go b/model/user.go index a8fb7842..5205662e 100644 --- a/model/user.go +++ b/model/user.go @@ -26,6 +26,8 @@ type User struct { UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number Group string `json:"group" gorm:"type:varchar(32);default:'default'"` + AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"` + InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` } func GetMaxUserId() int { @@ -58,6 +60,15 @@ func GetUserById(id int, selectAll bool) (*User, error) { return &user, err } +func GetUserIdByAffCode(affCode string) (int, error) { + if affCode == "" { + return 0, errors.New("affCode 为空!") + } + var user User + err := DB.Select("id").First(&user, "aff_code = ?", affCode).Error + return user.Id, err +} + func DeleteUserById(id int) (err error) { if id == 0 { return errors.New("id 为空!") @@ -66,7 +77,7 @@ func DeleteUserById(id int) (err error) { return user.Delete() } -func (user *User) Insert() error { +func (user *User) Insert(inviterId int) error { var err error if user.Password != "" { user.Password, err = common.Password2Hash(user.Password) @@ -76,6 +87,7 @@ func (user *User) Insert() error { } user.Quota = common.QuotaForNewUser user.AccessToken = common.GetUUID() + user.AffCode = common.GetRandomString(4) result := DB.Create(user) if result.Error != nil { return result.Error @@ -83,6 +95,16 @@ func (user *User) Insert() error { if common.QuotaForNewUser > 0 { RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %d 点额度", common.QuotaForNewUser)) } + if inviterId != 0 { + if common.QuotaForInvitee > 0 { + _ = IncreaseUserQuota(user.Id, common.QuotaForInvitee) + RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %d 点额度", common.QuotaForInvitee)) + } + if common.QuotaForInviter > 0 { + _ = IncreaseUserQuota(inviterId, common.QuotaForInviter) + RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %d 点额度", common.QuotaForInviter)) + } + } return nil } diff --git a/router/api-router.go b/router/api-router.go index 062ccac1..2e5cd7d4 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -37,6 +37,7 @@ func SetApiRouter(router *gin.Engine) { selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.DELETE("/self", controller.DeleteSelf) selfRoute.GET("/token", controller.GenerateAccessToken) + selfRoute.GET("/aff", controller.GetAffCode) selfRoute.POST("/topup", controller.TopUp) } diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index d3216811..6c6ea49f 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; -import { API, copy, showError, showInfo, showSuccess } from '../helpers'; +import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; import Turnstile from 'react-turnstile'; const PersonalSetting = () => { @@ -45,6 +45,18 @@ const PersonalSetting = () => { } }; + const getAffLink = async () => { + const res = await API.get('/api/user/aff'); + const { success, message, data } = res.data; + if (success) { + let link = `${window.location.origin}/register?aff=${data}`; + await copy(link); + showNotice(`邀请链接已复制到剪切板:${link}`); + } else { + showError(message); + } + }; + const bindWeChat = async () => { if (inputs.wechat_verification_code === '') return; const res = await API.get( @@ -110,6 +122,7 @@ const PersonalSetting = () => { 更新个人信息 +
账号绑定
{ diff --git a/web/src/components/RegisterForm.js b/web/src/components/RegisterForm.js index 666b5d3f..d70869e6 100644 --- a/web/src/components/RegisterForm.js +++ b/web/src/components/RegisterForm.js @@ -27,6 +27,10 @@ const RegisterForm = () => { const [turnstileToken, setTurnstileToken] = useState(''); const [loading, setLoading] = useState(false); const logo = getLogo(); + let affCode = new URLSearchParams(window.location.search).get('aff'); + if (affCode) { + localStorage.setItem('aff', affCode); + } useEffect(() => { let status = localStorage.getItem('status'); @@ -63,6 +67,10 @@ const RegisterForm = () => { return; } setLoading(true); + if (!affCode) { + affCode = localStorage.getItem('aff'); + } + inputs.aff_code = affCode; const res = await API.post( `/api/user/register?turnstile=${turnstileToken}`, inputs diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index ac6d9263..f771af7a 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -27,6 +27,8 @@ const SystemSetting = () => { TurnstileSecretKey: '', RegisterEnabled: '', QuotaForNewUser: 0, + QuotaForInviter: 0, + QuotaForInvitee: 0, QuotaRemindThreshold: 0, PreConsumedQuota: 0, ModelRatio: '', @@ -34,7 +36,7 @@ const SystemSetting = () => { TopUpLink: '', AutomaticDisableChannelEnabled: '', ChannelDisableThreshold: 0, - LogConsumeEnabled: '', + LogConsumeEnabled: '' }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); @@ -101,6 +103,8 @@ const SystemSetting = () => { name === 'TurnstileSiteKey' || name === 'TurnstileSecretKey' || name === 'QuotaForNewUser' || + name === 'QuotaForInviter' || + name === 'QuotaForInvitee' || name === 'QuotaRemindThreshold' || name === 'PreConsumedQuota' || name === 'ModelRatio' || @@ -122,6 +126,12 @@ const SystemSetting = () => { if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) { await updateOption('QuotaForNewUser', inputs.QuotaForNewUser); } + if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) { + await updateOption('QuotaForInvitee', inputs.QuotaForInvitee); + } + if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) { + await updateOption('QuotaForInviter', inputs.QuotaForInviter); + } if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) { await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); } @@ -329,6 +339,28 @@ const SystemSetting = () => { placeholder='请求结束后多退少补' /> + + + + Date: Sat, 17 Jun 2023 19:08:13 +0800 Subject: [PATCH 076/410] docs: update README (close #175) --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 49e9f421..6c4f079d 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,12 @@ sudo service nginx restart 环境变量的具体使用方法详见[此处](#环境变量)。 +### 宝塔部署教程 + +详见[#175](https://github.com/songquanpeng/one-api/issues/175)。 + +如果部署后访问出现空白页面,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。 + ### 部署第三方服务配合 One API 使用 > 欢迎 PR 添加更多示例。 @@ -259,9 +265,7 @@ https://openai.justsong.cn 1. 账户额度足够为什么提示额度不足? + 请检查你的令牌额度是否足够,这个和账户额度是分开的。 + 令牌额度仅供用户设置最大使用量,用户可自由设置。 -2. 宝塔部署后访问出现空白页面? - + 自动配置的问题,详见[#97](https://github.com/songquanpeng/one-api/issues/97)。 -3. 提示无可用渠道? +2. 提示无可用渠道? + 请检查的用户分组和渠道分组设置。 + 以及渠道的模型设置。 From bcbfacc04a885db3beee1801255cafe2cad71f63 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 19:23:25 +0800 Subject: [PATCH 077/410] fix: reduce the table size (close #174) --- web/src/components/ChannelsTable.js | 2 +- web/src/components/RedemptionsTable.js | 2 +- web/src/components/TokensTable.js | 2 +- web/src/components/UsersTable.js | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 7bc683ea..5f1c1d33 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -262,7 +262,7 @@ const ChannelsTable = () => { /> - +
{ /> -
+
{ /> -
+
{ /> -
+
{ /> {renderGroup(user.group)} - {user.email ? renderText(user.email, 20) : '无'} + + {user.email ? {renderText(user.email, 24)}} /> : '无'} + {renderNumber(user.quota)}} /> {renderNumber(user.used_quota)}} /> From 8e805e23bc8b048b53cc76592f56bb3001c11519 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 19:33:25 +0800 Subject: [PATCH 078/410] chore: update prompt --- web/src/components/OtherSetting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/OtherSetting.js b/web/src/components/OtherSetting.js index bfe36d6b..8d63c2dd 100644 --- a/web/src/components/OtherSetting.js +++ b/web/src/components/OtherSetting.js @@ -165,7 +165,7 @@ const OtherSetting = () => { /> 保存关于 - 移除 One API 的版权标识必须首先获得授权,后续版本将通过授权码强制执行。 + 移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。 Date: Sat, 17 Jun 2023 22:56:12 +0800 Subject: [PATCH 079/410] docs: update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6c4f079d..07d2ac39 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,9 @@ https://openai.justsong.cn 2. 提示无可用渠道? + 请检查的用户分组和渠道分组设置。 + 以及渠道的模型设置。 +3. 渠道测试报错:`invalid character '<' looking for beginning of value` + + 这是因为返回值不是合法的 JSON,而是一个 HTML 页面。 + + 大概率是你的部署站的 IP 或代理的节点被 CloudFlare 封禁了。 ## 注意 本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及法律法规的情况下使用,不得用于非法用途。 From a680b1b8b7923ab0a14f432473304f48e85af039 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 22:57:46 +0800 Subject: [PATCH 080/410] docs: update issue template --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 045004b8..2a89f0a2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,6 +10,7 @@ assignees: '' **例行检查** + [ ] 我已确认目前没有类似 issue + [ ] 我已确认我已升级到最新版本 ++ [ ] 我已完整查看过项目 README,尤其是常见问题部分 + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 From ba89abedf040a3227e72284998bd3b7651aa1992 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 23:18:27 +0800 Subject: [PATCH 081/410] docs: update README --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 07d2ac39..5c70cc3d 100644 --- a/README.md +++ b/README.md @@ -262,13 +262,16 @@ https://openai.justsong.cn ![token](https://user-images.githubusercontent.com/39998050/233837971-dab488b7-6d96-43af-b640-a168e8d1c9bf.png) ## 常见问题 -1. 账户额度足够为什么提示额度不足? +1. 额度是什么?怎么计算的? + + 额度 = token * 倍率 + + 倍率包括分组的倍率,以及补全的倍率。 +2. 账户额度足够为什么提示额度不足? + 请检查你的令牌额度是否足够,这个和账户额度是分开的。 + 令牌额度仅供用户设置最大使用量,用户可自由设置。 -2. 提示无可用渠道? +3. 提示无可用渠道? + 请检查的用户分组和渠道分组设置。 + 以及渠道的模型设置。 -3. 渠道测试报错:`invalid character '<' looking for beginning of value` +4. 渠道测试报错:`invalid character '<' looking for beginning of value` + 这是因为返回值不是合法的 JSON,而是一个 HTML 页面。 + 大概率是你的部署站的 IP 或代理的节点被 CloudFlare 封禁了。 From d97640374c8a248484cf243b332a0a18d2e9982b Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 23:51:56 +0800 Subject: [PATCH 082/410] feat: able to add chat page link now (close #70) --- common/constants.go | 1 + controller/misc.go | 1 + model/option.go | 3 +++ web/src/App.js | 14 ++++++++++ web/src/components/Header.js | 40 +++++++++++++++++------------ web/src/components/SystemSetting.js | 16 +++++++++++- web/src/pages/Chat/index.js | 15 +++++++++++ 7 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 web/src/pages/Chat/index.js diff --git a/common/constants.go b/common/constants.go index 471f6ff2..7a1694c5 100644 --- a/common/constants.go +++ b/common/constants.go @@ -14,6 +14,7 @@ var ServerAddress = "http://localhost:3000" var Footer = "" var Logo = "" var TopUpLink = "" +var ChatLink = "" var UsingSQLite = false diff --git a/controller/misc.go b/controller/misc.go index d200a239..10f1f99e 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -28,6 +28,7 @@ func GetStatus(c *gin.Context) { "turnstile_check": common.TurnstileCheckEnabled, "turnstile_site_key": common.TurnstileSiteKey, "top_up_link": common.TopUpLink, + "chat_link": common.ChatLink, }, }) return diff --git a/model/option.go b/model/option.go index 32d655ac..b53b172d 100644 --- a/model/option.go +++ b/model/option.go @@ -63,6 +63,7 @@ func InitOptionMap() { common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString() common.OptionMap["TopUpLink"] = common.TopUpLink + common.OptionMap["ChatLink"] = common.ChatLink common.OptionMapRWMutex.Unlock() loadOptionsFromDatabase() } @@ -191,6 +192,8 @@ func updateOptionMap(key string, value string) (err error) { err = common.UpdateGroupRatioByJSONString(value) case "TopUpLink": common.TopUpLink = value + case "ChatLink": + common.ChatLink = value case "ChannelDisableThreshold": common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64) } diff --git a/web/src/App.js b/web/src/App.js index e404b7f3..61477141 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -23,6 +23,7 @@ import Redemption from './pages/Redemption'; import EditRedemption from './pages/Redemption/EditRedemption'; import TopUp from './pages/TopUp'; import Log from './pages/Log'; +import Chat from './pages/Chat'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -47,6 +48,11 @@ function App() { localStorage.setItem('system_name', data.system_name); localStorage.setItem('logo', data.logo); localStorage.setItem('footer_html', data.footer_html); + if (data.chat_link) { + localStorage.setItem('chat_link', data.chat_link); + } else { + localStorage.removeItem('chat_link'); + } if ( data.version !== process.env.REACT_APP_VERSION && data.version !== 'v0.0.0' && @@ -267,6 +273,14 @@ function App() { } /> + }> + + + } + /> ); diff --git a/web/src/components/Header.js b/web/src/components/Header.js index 3f4e32c1..21ebcab6 100644 --- a/web/src/components/Header.js +++ b/web/src/components/Header.js @@ -7,57 +7,65 @@ import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../ import '../index.css'; // Header Buttons -const headerButtons = [ +let headerButtons = [ { name: '首页', to: '/', - icon: 'home', + icon: 'home' }, { name: '渠道', to: '/channel', icon: 'sitemap', - admin: true, + admin: true }, { name: '令牌', to: '/token', - icon: 'key', + icon: 'key' }, { name: '兑换', to: '/redemption', icon: 'dollar sign', - admin: true, + admin: true }, { name: '充值', to: '/topup', - icon: 'cart', + icon: 'cart' }, { name: '用户', to: '/user', icon: 'user', - admin: true, + admin: true }, { name: '日志', to: '/log', - icon: 'book', + icon: 'book' }, { name: '设置', to: '/setting', - icon: 'setting', + icon: 'setting' }, { name: '关于', to: '/about', - icon: 'info circle', - }, + icon: 'info circle' + } ]; +if (localStorage.getItem('chat_link')) { + headerButtons.splice(1, 0, { + name: '聊天', + to: '/chat', + icon: 'comments' + }); +} + const Header = () => { const [userState, userDispatch] = useContext(UserContext); let navigate = useNavigate(); @@ -112,11 +120,11 @@ const Header = () => { style={ showSidebar ? { - borderBottom: 'none', - marginBottom: '0', - borderTop: 'none', - height: '51px', - } + borderBottom: 'none', + marginBottom: '0', + borderTop: 'none', + height: '51px' + } : { borderTop: 'none', height: '52px' } } > diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index f771af7a..786e935b 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -34,6 +34,7 @@ const SystemSetting = () => { ModelRatio: '', GroupRatio: '', TopUpLink: '', + ChatLink: '', AutomaticDisableChannelEnabled: '', ChannelDisableThreshold: 0, LogConsumeEnabled: '' @@ -109,7 +110,8 @@ const SystemSetting = () => { name === 'PreConsumedQuota' || name === 'ModelRatio' || name === 'GroupRatio' || - name === 'TopUpLink' + name === 'TopUpLink' || + name === 'ChatLink' ) { setInputs((inputs) => ({ ...inputs, [name]: value })); } else { @@ -155,6 +157,9 @@ const SystemSetting = () => { if (originInputs['TopUpLink'] !== inputs.TopUpLink) { await updateOption('TopUpLink', inputs.TopUpLink); } + if (originInputs['ChatLink'] !== inputs.ChatLink) { + await updateOption('ChatLink', inputs.ChatLink); + } }; const submitSMTP = async () => { @@ -360,6 +365,15 @@ const SystemSetting = () => { min='0' placeholder='例如:100' /> + { + const chatLink = localStorage.getItem('chat_link'); + + return ( +