重构页面布局,新增标签页导航功能,整合提供商管理、虚拟模型和请求日志页面

This commit is contained in:
2025-11-09 02:09:12 +08:00
parent 1e2cf83ff0
commit ee281f8af9
5 changed files with 207 additions and 13 deletions

View File

@@ -140,6 +140,7 @@ func createRequestLog(apiKeyID uint, virtualModelName string, backendModel *mode
APIKeyID: apiKeyID,
VirtualModelName: virtualModelName,
BackendModelName: backendModel.Name,
ProviderName: backendModel.Provider.Name,
RequestTimestamp: requestTimestamp,
ResponseTimestamp: responseTimestamp,
RequestTokens: requestTokenCount,
@@ -172,3 +173,155 @@ func copyResponseHeaders(c *gin.Context, resp *http.Response) {
}
}
}
// GetRequestLogsHandler 获取请求日志列表
func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) {
// 获取分页参数
page := 1
pageSize := 20
if pageParam := c.Query("page"); pageParam != "" {
fmt.Sscanf(pageParam, "%d", &page)
}
if pageSizeParam := c.Query("page_size"); pageSizeParam != "" {
fmt.Sscanf(pageSizeParam, "%d", &pageSize)
}
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
// 构建查询
query := h.DB.Model(&models.RequestLog{})
// 按时间范围过滤
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse(time.RFC3339, startTime); err == nil {
query = query.Where("request_timestamp >= ?", t)
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse(time.RFC3339, endTime); err == nil {
query = query.Where("request_timestamp <= ?", t)
}
}
// 按虚拟模型名称过滤
if virtualModel := c.Query("virtual_model"); virtualModel != "" {
query = query.Where("virtual_model_name = ?", virtualModel)
}
// 按后端模型名称过滤
if backendModel := c.Query("backend_model"); backendModel != "" {
query = query.Where("backend_model_name = ?", backendModel)
}
// 获取总数
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count logs"})
return
}
// 获取日志列表
var logs []models.RequestLog
offset := (page - 1) * pageSize
if err := query.Order("request_timestamp DESC").Limit(pageSize).Offset(offset).Find(&logs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch logs"})
return
}
c.JSON(http.StatusOK, gin.H{
"logs": logs,
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
})
}
// GetRequestLogStatsHandler 获取请求日志统计信息
func (h *APIHandler) GetRequestLogStatsHandler(c *gin.Context) {
// 获取时间范围参数
var startTime, endTime time.Time
var err error
if startTimeParam := c.Query("start_time"); startTimeParam != "" {
startTime, err = time.Parse(time.RFC3339, startTimeParam)
if err != nil {
// 默认最近7天
startTime = time.Now().AddDate(0, 0, -7)
}
} else {
startTime = time.Now().AddDate(0, 0, -7)
}
if endTimeParam := c.Query("end_time"); endTimeParam != "" {
endTime, err = time.Parse(time.RFC3339, endTimeParam)
if err != nil {
endTime = time.Now()
}
} else {
endTime = time.Now()
}
// 统计总请求数
var totalRequests int64
h.DB.Model(&models.RequestLog{}).
Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
Count(&totalRequests)
// 统计总成本
var totalCost float64
h.DB.Model(&models.RequestLog{}).
Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
Select("COALESCE(SUM(cost), 0)").
Scan(&totalCost)
// 统计总token数
var totalTokens struct {
RequestTokens int64
ResponseTokens int64
}
h.DB.Model(&models.RequestLog{}).
Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
Select("COALESCE(SUM(request_tokens), 0) as request_tokens, COALESCE(SUM(response_tokens), 0) as response_tokens").
Scan(&totalTokens)
// 按虚拟模型统计
type ModelStats struct {
ModelName string `json:"model_name"`
Count int64 `json:"count"`
TotalCost float64 `json:"total_cost"`
}
var virtualModelStats []ModelStats
h.DB.Model(&models.RequestLog{}).
Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
Select("virtual_model_name as model_name, COUNT(*) as count, COALESCE(SUM(cost), 0) as total_cost").
Group("virtual_model_name").
Order("count DESC").
Limit(10).
Scan(&virtualModelStats)
// 按后端模型统计
var backendModelStats []ModelStats
h.DB.Model(&models.RequestLog{}).
Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
Select("backend_model_name as model_name, COUNT(*) as count, COALESCE(SUM(cost), 0) as total_cost").
Group("backend_model_name").
Order("count DESC").
Limit(10).
Scan(&backendModelStats)
c.JSON(http.StatusOK, gin.H{
"start_time": startTime,
"end_time": endTime,
"total_requests": totalRequests,
"total_cost": totalCost,
"total_request_tokens": totalTokens.RequestTokens,
"total_response_tokens": totalTokens.ResponseTokens,
"virtual_model_stats": virtualModelStats,
"backend_model_stats": backendModelStats,
})
}

View File

@@ -54,6 +54,7 @@ type BackendModel struct {
type RequestLog struct {
gorm.Model
APIKeyID uint `gorm:"index"` // API密钥ID
ProviderName string `gorm:"index"` // 服务商名称
VirtualModelName string `gorm:"index"` // 虚拟模型名称
BackendModelName string `gorm:"index"` // 后端模型名称
RequestTimestamp time.Time `gorm:"index;not null"` // 请求时间戳

View File

@@ -72,6 +72,10 @@ func main() {
api_.GET("/virtual-models/:id", handler.GetVirtualModelHandler)
api_.PUT("/virtual-models/:id", handler.UpdateVirtualModelHandler)
api_.DELETE("/virtual-models/:id", handler.DeleteVirtualModelHandler)
// Request Logs
api_.GET("/logs", handler.GetRequestLogsHandler)
api_.GET("/logs/stats", handler.GetRequestLogStatsHandler)
}
// 启动HTTP服务器

View File

@@ -2,17 +2,26 @@ import React from 'react';
import PageLayout from './components/layout/PageLayout';
import ProvidersPage from './features/providers';
import VirtualModelsPage from './features/virtual-models';
import LogsPage from './features/logs';
import './App.css';
function App() {
return (
<PageLayout>
<div className="space-y-8">
<ProvidersPage />
<VirtualModelsPage />
</div>
</PageLayout>
);
const tabs = [
{
label: '提供商管理',
content: <ProvidersPage />
},
{
label: '虚拟模型',
content: <VirtualModelsPage />
},
{
label: '请求日志',
content: <LogsPage />
}
];
return <PageLayout tabs={tabs} />;
}
export default App;

View File

@@ -1,12 +1,39 @@
import React from 'react';
import React, { useState } from 'react';
const PageLayout = ({ tabs }) => {
const [activeTab, setActiveTab] = useState(0);
const PageLayout = ({ children }) => {
return (
<div className="container mx-auto p-4">
<header className="mb-4">
<h1 className="text-2xl font-bold">AI Gateway Config</h1>
<header className="mb-6">
<h1 className="text-2xl font-bold mb-4">AI Gateway Config</h1>
{/* 标签页导航 */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab, index) => (
<button
key={index}
onClick={() => setActiveTab(index)}
className={`
py-2 px-1 border-b-2 font-medium text-sm transition-colors
${activeTab === index
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}
`}
>
{tab.label}
</button>
))}
</nav>
</div>
</header>
<main>{children}</main>
{/* 标签页内容 */}
<main className="mt-6">
{tabs[activeTab]?.content}
</main>
</div>
);
};