mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 22:03:41 +08:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			v0.4.0
			...
			v0.4.1-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 74f508e847 | ||
|  | 145bb14cb2 | ||
|  | 8901f03864 | ||
|  | 813bf0bd66 | ||
|  | 45e9fd66e7 | 
| @@ -46,9 +46,11 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | |||||||
|  |  | ||||||
| > **Warning**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 | > **Warning**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 | ||||||
|  |  | ||||||
|  | > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 | ||||||
|  |  | ||||||
| ## 功能 | ## 功能 | ||||||
| 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: | 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: | ||||||
|    + [x] OpenAI 官方通道 |    + [x] OpenAI 官方通道(支持配置代理) | ||||||
|    + [x] **Azure OpenAI API** |    + [x] **Azure OpenAI API** | ||||||
|    + [x] [API2D](https://api2d.com/r/197971) |    + [x] [API2D](https://api2d.com/r/197971) | ||||||
|    + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) |    + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) | ||||||
| @@ -57,7 +59,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | |||||||
|    + [x] [OpenAI Max](https://openaimax.com) |    + [x] [OpenAI Max](https://openaimax.com) | ||||||
|    + [x] [OpenAI-SB](https://openai-sb.com) |    + [x] [OpenAI-SB](https://openai-sb.com) | ||||||
|    + [x] [CloseAI](https://console.openai-asia.com/r/2412) |    + [x] [CloseAI](https://console.openai-asia.com/r/2412) | ||||||
|    + [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理 |    + [x] 自定义渠道:例如各种未收录的第三方代理服务 | ||||||
| 2. 支持通过**负载均衡**的方式访问多个渠道。 | 2. 支持通过**负载均衡**的方式访问多个渠道。 | ||||||
| 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | ||||||
| 4. 支持**多机部署**,[详见此处](#多机部署)。 | 4. 支持**多机部署**,[详见此处](#多机部署)。 | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								bin/migration_v0.3-v0.4.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								bin/migration_v0.3-v0.4.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | ); | ||||||
| @@ -25,6 +25,7 @@ var OptionMap map[string]string | |||||||
| var OptionMapRWMutex sync.RWMutex | var OptionMapRWMutex sync.RWMutex | ||||||
|  |  | ||||||
| var ItemsPerPage = 10 | var ItemsPerPage = 10 | ||||||
|  | var MaxRecentItems = 100 | ||||||
|  |  | ||||||
| var PasswordLoginEnabled = true | var PasswordLoginEnabled = true | ||||||
| var PasswordRegisterEnabled = true | var PasswordRegisterEnabled = true | ||||||
|   | |||||||
| @@ -41,7 +41,9 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { | |||||||
| 	baseURL := common.ChannelBaseURLs[channel.Type] | 	baseURL := common.ChannelBaseURLs[channel.Type] | ||||||
| 	switch channel.Type { | 	switch channel.Type { | ||||||
| 	case common.ChannelTypeOpenAI: | 	case common.ChannelTypeOpenAI: | ||||||
| 		// do nothing | 		if channel.BaseURL != "" { | ||||||
|  | 			baseURL = channel.BaseURL | ||||||
|  | 		} | ||||||
| 	case common.ChannelTypeAzure: | 	case common.ChannelTypeAzure: | ||||||
| 		return 0, errors.New("尚未实现") | 		return 0, errors.New("尚未实现") | ||||||
| 	case common.ChannelTypeCustom: | 	case common.ChannelTypeCustom: | ||||||
|   | |||||||
| @@ -27,6 +27,8 @@ func testChannel(channel *model.Channel, request *ChatRequest) error { | |||||||
| 	} else { | 	} else { | ||||||
| 		if channel.Type == common.ChannelTypeCustom { | 		if channel.Type == common.ChannelTypeCustom { | ||||||
| 			requestURL = channel.BaseURL | 			requestURL = channel.BaseURL | ||||||
|  | 		} else if channel.Type == common.ChannelTypeOpenAI && channel.BaseURL != "" { | ||||||
|  | 			requestURL = channel.BaseURL | ||||||
| 		} | 		} | ||||||
| 		requestURL += "/v1/chat/completions" | 		requestURL += "/v1/chat/completions" | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										86
									
								
								controller/log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								controller/log.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -147,6 +147,10 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	requestURL := c.Request.URL.String() | 	requestURL := c.Request.URL.String() | ||||||
| 	if channelType == common.ChannelTypeCustom { | 	if channelType == common.ChannelTypeCustom { | ||||||
| 		baseURL = c.GetString("base_url") | 		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) | 	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) | ||||||
| 	if channelType == common.ChannelTypeAzure { | 	if channelType == common.ChannelTypeAzure { | ||||||
| @@ -240,6 +244,8 @@ func relayHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				common.SysError("Error consuming token remain quota: " + err.Error()) | 				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)) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -82,11 +82,9 @@ func Distribute() func(c *gin.Context) { | |||||||
| 		c.Set("channel_id", channel.Id) | 		c.Set("channel_id", channel.Id) | ||||||
| 		c.Set("channel_name", channel.Name) | 		c.Set("channel_name", channel.Name) | ||||||
| 		c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) | 		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) | ||||||
| 			c.Set("base_url", channel.BaseURL) | 		if channel.Type == common.ChannelTypeAzure { | ||||||
| 			if channel.Type == common.ChannelTypeAzure { | 			c.Set("api_version", channel.Other) | ||||||
| 				c.Set("api_version", channel.Other) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 		c.Next() | 		c.Next() | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -13,9 +13,6 @@ type Ability struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { | func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { | ||||||
| 	if group == "default" { |  | ||||||
| 		return GetRandomChannel() |  | ||||||
| 	} |  | ||||||
| 	ability := Ability{} | 	ability := Ability{} | ||||||
| 	var err error = nil | 	var err error = nil | ||||||
| 	if common.UsingSQLite { | 	if common.UsingSQLite { | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								model/log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								model/log.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | package model | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | 	"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"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	LogTypeUnknown = iota | ||||||
|  | 	LogTypeTopup | ||||||
|  | 	LogTypeConsume | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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) { | ||||||
|  | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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).Omit("id").Find(&logs).Error | ||||||
|  | 	return logs, err | ||||||
|  | } | ||||||
| @@ -79,6 +79,10 @@ func InitDB() (err error) { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | 		err = db.AutoMigrate(&Log{}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
| 		err = createRootAccountIfNeed() | 		err = createRootAccountIfNeed() | ||||||
| 		return err | 		return err | ||||||
| 	} else { | 	} else { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package model | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -65,6 +66,7 @@ func Redeem(key string, userId int) (quota int, err error) { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			common.SysError("更新兑换码状态失败:" + err.Error()) | 			common.SysError("更新兑换码状态失败:" + err.Error()) | ||||||
| 		} | 		} | ||||||
|  | 		RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %d 点额度", redemption.Quota)) | ||||||
| 	}() | 	}() | ||||||
| 	return redemption.Quota, nil | 	return redemption.Quota, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -93,5 +93,10 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 			redemptionRoute.PUT("/", controller.UpdateRedemption) | 			redemptionRoute.PUT("/", controller.UpdateRedemption) | ||||||
| 			redemptionRoute.DELETE("/:id", controller.DeleteRedemption) | 			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) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ import EditChannel from './pages/Channel/EditChannel'; | |||||||
| import Redemption from './pages/Redemption'; | import Redemption from './pages/Redemption'; | ||||||
| import EditRedemption from './pages/Redemption/EditRedemption'; | import EditRedemption from './pages/Redemption/EditRedemption'; | ||||||
| import TopUp from './pages/TopUp'; | import TopUp from './pages/TopUp'; | ||||||
|  | import Log from './pages/Log'; | ||||||
|  |  | ||||||
| const Home = lazy(() => import('./pages/Home')); | const Home = lazy(() => import('./pages/Home')); | ||||||
| const About = lazy(() => import('./pages/About')); | const About = lazy(() => import('./pages/About')); | ||||||
| @@ -250,6 +251,14 @@ function App() { | |||||||
|         </PrivateRoute> |         </PrivateRoute> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|  |       <Route | ||||||
|  |         path='/log' | ||||||
|  |         element={ | ||||||
|  |           <PrivateRoute> | ||||||
|  |             <Log /> | ||||||
|  |           </PrivateRoute> | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|       <Route |       <Route | ||||||
|         path='/about' |         path='/about' | ||||||
|         element={ |         element={ | ||||||
|   | |||||||
| @@ -41,6 +41,11 @@ const headerButtons = [ | |||||||
|     icon: 'user', |     icon: 'user', | ||||||
|     admin: true, |     admin: true, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     name: '日志', | ||||||
|  |     to: '/log', | ||||||
|  |     icon: 'book', | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     name: '设置', |     name: '设置', | ||||||
|     to: '/setting', |     to: '/setting', | ||||||
|   | |||||||
							
								
								
									
										186
									
								
								web/src/components/LogsTable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								web/src/components/LogsTable.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | |||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | import { Button, Label, Pagination, Table } from 'semantic-ui-react'; | ||||||
|  | import { API, showError, timestamp2string } from '../helpers'; | ||||||
|  |  | ||||||
|  | import { ITEMS_PER_PAGE } from '../constants'; | ||||||
|  |  | ||||||
|  | function renderTimestamp(timestamp) { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {timestamp2string(timestamp)} | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function renderType(type) { | ||||||
|  |   switch (type) { | ||||||
|  |     case 1: | ||||||
|  |       return <Label basic color='green'> 充值 </Label>; | ||||||
|  |     case 2: | ||||||
|  |       return <Label basic color='olive'> 消费 </Label>; | ||||||
|  |     default: | ||||||
|  |       return <Label basic color='black'> 未知 </Label>; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 ( | ||||||
|  |     <> | ||||||
|  |       <Table basic> | ||||||
|  |         <Table.Header> | ||||||
|  |           <Table.Row> | ||||||
|  |             <Table.HeaderCell | ||||||
|  |               style={{ cursor: 'pointer' }} | ||||||
|  |               onClick={() => { | ||||||
|  |                 sortLog('created_time'); | ||||||
|  |               }} | ||||||
|  |               width={3} | ||||||
|  |             > | ||||||
|  |               时间 | ||||||
|  |             </Table.HeaderCell> | ||||||
|  |             <Table.HeaderCell | ||||||
|  |               style={{ cursor: 'pointer' }} | ||||||
|  |               onClick={() => { | ||||||
|  |                 sortLog('type'); | ||||||
|  |               }} | ||||||
|  |               width={2} | ||||||
|  |             > | ||||||
|  |               类型 | ||||||
|  |             </Table.HeaderCell> | ||||||
|  |             <Table.HeaderCell | ||||||
|  |               style={{ cursor: 'pointer' }} | ||||||
|  |               onClick={() => { | ||||||
|  |                 sortLog('content'); | ||||||
|  |               }} | ||||||
|  |               width={11} | ||||||
|  |             > | ||||||
|  |               详情 | ||||||
|  |             </Table.HeaderCell> | ||||||
|  |           </Table.Row> | ||||||
|  |         </Table.Header> | ||||||
|  |  | ||||||
|  |         <Table.Body> | ||||||
|  |           {logs | ||||||
|  |             .slice( | ||||||
|  |               (activePage - 1) * ITEMS_PER_PAGE, | ||||||
|  |               activePage * ITEMS_PER_PAGE | ||||||
|  |             ) | ||||||
|  |             .map((log, idx) => { | ||||||
|  |               if (log.deleted) return <></>; | ||||||
|  |               return ( | ||||||
|  |                 <Table.Row key={log.created_at}> | ||||||
|  |                   <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell> | ||||||
|  |                   <Table.Cell>{renderType(log.type)}</Table.Cell> | ||||||
|  |                   <Table.Cell>{log.content}</Table.Cell> | ||||||
|  |                 </Table.Row> | ||||||
|  |               ); | ||||||
|  |             })} | ||||||
|  |         </Table.Body> | ||||||
|  |  | ||||||
|  |         <Table.Footer> | ||||||
|  |           <Table.Row> | ||||||
|  |             <Table.HeaderCell colSpan='4'> | ||||||
|  |               <Button size='small' onClick={refresh} loading={loading}>刷新</Button> | ||||||
|  |               <Pagination | ||||||
|  |                 floated='right' | ||||||
|  |                 activePage={activePage} | ||||||
|  |                 onPageChange={onPaginationChange} | ||||||
|  |                 size='small' | ||||||
|  |                 siblingRange={1} | ||||||
|  |                 totalPages={ | ||||||
|  |                   Math.ceil(logs.length / ITEMS_PER_PAGE) + | ||||||
|  |                   (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0) | ||||||
|  |                 } | ||||||
|  |               /> | ||||||
|  |             </Table.HeaderCell> | ||||||
|  |           </Table.Row> | ||||||
|  |         </Table.Footer> | ||||||
|  |       </Table> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default LogsTable; | ||||||
| @@ -198,6 +198,20 @@ const EditChannel = () => { | |||||||
|               handleInputChange(null, { name: 'models', value: fullModels }); |               handleInputChange(null, { name: 'models', value: fullModels }); | ||||||
|             }}>填入所有模型</Button> |             }}>填入所有模型</Button> | ||||||
|           </div> |           </div> | ||||||
|  |           { | ||||||
|  |             inputs.type === 1 && ( | ||||||
|  |               <Form.Field> | ||||||
|  |                 <Form.Input | ||||||
|  |                   label='代理' | ||||||
|  |                   name='base_url' | ||||||
|  |                   placeholder={'请输入 OpenAI API 代理地址,如果不需要请留空,格式为:https://api.openai.com'} | ||||||
|  |                   onChange={handleInputChange} | ||||||
|  |                   value={inputs.base_url} | ||||||
|  |                   autoComplete='new-password' | ||||||
|  |                 /> | ||||||
|  |               </Form.Field> | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|           { |           { | ||||||
|             batch ? <Form.Field> |             batch ? <Form.Field> | ||||||
|               <Form.TextArea |               <Form.TextArea | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								web/src/pages/Log/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web/src/pages/Log/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { Header, Segment } from 'semantic-ui-react'; | ||||||
|  | import LogsTable from '../../components/LogsTable'; | ||||||
|  |  | ||||||
|  | const Token = () => ( | ||||||
|  |   <> | ||||||
|  |     <Segment> | ||||||
|  |       <Header as='h3'>额度明细</Header> | ||||||
|  |       <LogsTable /> | ||||||
|  |     </Segment> | ||||||
|  |   </> | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export default Token; | ||||||
		Reference in New Issue
	
	Block a user