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。 + +
+
+ ## 配置 系统本身开箱即用。 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 7bc494a9..c6257423 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.toFixed(5)} + switch (type) { + case 1: // OpenAI + case 8: // 自定义 + return ${balance.toFixed(2)}; + case 5: // OpenAI-SB + return {balance.toFixed(5)}; + case 10: // AI Proxy + return {renderNumber(balance)}; + default: + return 不支持; } - return ${balance.toFixed(2)} } const ChannelsTable = () => { @@ -440,14 +447,15 @@ const ChannelsTable = () => { - + - + { 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 的版权标识必须首先获得授权,后续版本将通过授权码强制执行。