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 的版权标识必须首先获得授权,后续版本将通过授权码强制执行。