mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 05:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			v0.4.4-alp
			...
			v0.4.4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d79289ccdd | ||
|  | f89f6c7fa6 | ||
|  | b7d71b4f0a | ||
|  | 70ed126ccb | ||
|  | 57b213a035 | ||
|  | 549e944b95 | 
							
								
								
									
										42
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								README.md
									
									
									
									
									
								
							| @@ -29,10 +29,10 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | ||||
| </p> | ||||
|  | ||||
| <p align="center"> | ||||
|   <a href="https://github.com/songquanpeng/one-api/releases">程序下载</a> | ||||
|   · | ||||
|   <a href="https://github.com/songquanpeng/one-api#部署">部署教程</a> | ||||
|   · | ||||
|   <a href="https://github.com/songquanpeng/one-api#使用方法">使用方法</a> | ||||
|   · | ||||
|   <a href="https://github.com/songquanpeng/one-api/issues">意见反馈</a> | ||||
|   · | ||||
|   <a href="https://github.com/songquanpeng/one-api#截图展示">截图展示</a> | ||||
| @@ -151,6 +151,27 @@ sudo service nginx restart | ||||
|  | ||||
| 环境变量的具体使用方法详见[此处](#环境变量)。 | ||||
|  | ||||
|  | ||||
| ### 部署到第三方平台 | ||||
| <details> | ||||
| <summary><strong>部署到 Zeabur</strong></summary> | ||||
| <div> | ||||
|  | ||||
| > 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`,值为 `<username>:<password>@tcp(<addr>:<port>)/one-api` ,然后保存。 注意如果不填写 `SQL_DSN`,数据将无法持久化,重新部署后数据会丢失。 | ||||
| 7. 选择 Redeploy。 | ||||
| 8. 进入下方 Domains,选择一个合适的域名前缀,如 "my-one-api",最终域名为 "my-one-api.zeabur.app",也可以 CNAME 自己的域名。 | ||||
| 9. 等待部署完成,点击生成的域名进入 One API。 | ||||
|  | ||||
| </div> | ||||
| </details> | ||||
|  | ||||
| ## 配置 | ||||
| 系统本身开箱即用。 | ||||
|  | ||||
| @@ -158,11 +179,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。 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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, | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	}) | ||||
|   | ||||
| @@ -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 <span>¥{(balance / 10000).toFixed(2)}</span> | ||||
|   switch (type) { | ||||
|     case 1: // OpenAI | ||||
|     case 8: // 自定义 | ||||
|       return <span>${balance.toFixed(2)}</span>; | ||||
|     case 5: // OpenAI-SB | ||||
|       return <span>¥{(balance / 10000).toFixed(2)}</span>; | ||||
|     case 10: // AI Proxy | ||||
|       return <span>{renderNumber(balance)}</span>; | ||||
|     default: | ||||
|       return <span>不支持</span>; | ||||
|   } | ||||
|   return <span>${balance.toFixed(2)}</span> | ||||
| } | ||||
|  | ||||
| const ChannelsTable = () => { | ||||
| @@ -422,7 +429,8 @@ const ChannelsTable = () => { | ||||
|               <Button size='small' loading={loading} onClick={testAllChannels}> | ||||
|                 测试所有已启用通道 | ||||
|               </Button> | ||||
|               <Button size='small' onClick={updateAllChannelsBalance} loading={loading || updatingBalance}>更新所有已启用通道余额</Button> | ||||
|               <Button size='small' onClick={updateAllChannelsBalance} | ||||
|                       loading={loading || updatingBalance}>更新所有已启用通道余额</Button> | ||||
|               <Pagination | ||||
|                 floated='right' | ||||
|                 activePage={activePage} | ||||
|   | ||||
| @@ -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 ( | ||||
|     <Segment vertical> | ||||
|   | ||||
| @@ -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' }} | ||||
|             /> | ||||
|           </Form.Group> | ||||
|           <Form.Button onClick={()=>submitOption('HomePageContent')}>保存首页内容</Form.Button> | ||||
|           <Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button> | ||||
|           <Form.Group widths='equal'> | ||||
|             <Form.TextArea | ||||
|               label='关于' | ||||
| @@ -165,6 +165,7 @@ const OtherSetting = () => { | ||||
|             /> | ||||
|           </Form.Group> | ||||
|           <Form.Button onClick={submitAbout}>保存关于</Form.Button> | ||||
|           <Message>移除 One API 的版权标识必须首先获得授权,后续版本将通过授权码强制执行。</Message> | ||||
|           <Form.Group widths='equal'> | ||||
|             <Form.Input | ||||
|               label='页脚' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user