From 70ed126ccb8a58fa00c73544198180c1354e1872 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 17 Jun 2023 09:46:07 +0800 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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 的版权标识必须首先获得授权,后续版本将通过授权码强制执行。