mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-05 01:14:56 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16599a900b | ||
|
|
d241e4fe29 | ||
|
|
e17f36e7b7 | ||
|
|
263547ebb7 | ||
|
|
5d338337a0 | ||
|
|
b1fb595610 | ||
|
|
44512d3c28 | ||
|
|
430d5fcd6a | ||
|
|
6625563f80 | ||
|
|
b2d36b946d | ||
|
|
ab4c9fdb8f | ||
|
|
79de02b05f |
4
go.mod
4
go.mod
@@ -11,7 +11,6 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
|
||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
|
||||
github.com/bytedance/sonic v1.12.4
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
@@ -42,7 +41,8 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
|
||||
github.com/aws/smithy-go v1.20.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -22,11 +22,10 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
|
||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
|
||||
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
|
||||
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
@@ -205,6 +204,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -270,3 +270,4 @@ gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
||||
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -85,7 +85,7 @@ func SaveQuotaDataCache() {
|
||||
//quotaDataDB.Count += quotaData.Count
|
||||
//quotaDataDB.Quota += quotaData.Quota
|
||||
//DB.Table("quota_data").Save(quotaDataDB)
|
||||
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt)
|
||||
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt, quotaData.TokenUsed)
|
||||
} else {
|
||||
DB.Table("quota_data").Create(quotaData)
|
||||
}
|
||||
@@ -94,11 +94,12 @@ func SaveQuotaDataCache() {
|
||||
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
|
||||
}
|
||||
|
||||
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64) {
|
||||
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64, tokenUsed int) {
|
||||
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
|
||||
userId, username, modelName, createdAt).Updates(map[string]interface{}{
|
||||
"count": gorm.Expr("count + ?", count),
|
||||
"quota": gorm.Expr("quota + ?", quota),
|
||||
"count": gorm.Expr("count + ?", count),
|
||||
"quota": gorm.Expr("quota + ?", quota),
|
||||
"token_used": gorm.Expr("token_used + ?", tokenUsed),
|
||||
}).Error
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
|
||||
@@ -127,6 +128,6 @@ func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaDat
|
||||
// 从quota_data表中查询数据
|
||||
// only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at;
|
||||
//err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find("aDatas).Error
|
||||
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error
|
||||
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error
|
||||
return quotaDatas, err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ var ModelList = []string{
|
||||
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
|
||||
"gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
|
||||
"gemini-exp-1114", "gemini-exp-1206",
|
||||
"gemini-2.0-flash-exp",
|
||||
}
|
||||
|
||||
var ChannelName = "google gemini"
|
||||
|
||||
@@ -34,6 +34,7 @@ type GeminiChatSafetySettings struct {
|
||||
}
|
||||
|
||||
type GeminiChatTools struct {
|
||||
GoogleSearch any `json:"googleSearch,omitempty"`
|
||||
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -45,13 +45,25 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
|
||||
}
|
||||
if textRequest.Tools != nil {
|
||||
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
|
||||
googleSearch := false
|
||||
for _, tool := range textRequest.Tools {
|
||||
if tool.Function.Name == "googleSearch" {
|
||||
googleSearch = true
|
||||
continue
|
||||
}
|
||||
functions = append(functions, tool.Function)
|
||||
}
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
{
|
||||
FunctionDeclarations: functions,
|
||||
},
|
||||
if len(functions) > 0 {
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
{
|
||||
FunctionDeclarations: functions,
|
||||
},
|
||||
}
|
||||
}
|
||||
if googleSearch {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTools{
|
||||
GoogleSearch: make(map[string]string),
|
||||
})
|
||||
}
|
||||
} else if textRequest.Functions != nil {
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
@@ -134,7 +146,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
|
||||
shouldAddDummyModelMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
return &geminiRequest
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -180,7 +178,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
|
||||
}
|
||||
jsonData, err := sonic.Marshal(convertedRequest)
|
||||
jsonData, err := json.Marshal(convertedRequest)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -124,11 +124,11 @@ const HeaderBar = () => {
|
||||
return (
|
||||
<div onClick={(e) => {
|
||||
if (props.itemKey === 'home') {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
}
|
||||
}}>
|
||||
<Link
|
||||
@@ -148,12 +148,12 @@ const HeaderBar = () => {
|
||||
logo: (
|
||||
<>
|
||||
{
|
||||
styleState.showSider ?
|
||||
!styleState.showSider ?
|
||||
<Button icon={<IconMenu />} theme="light" aria-label="展开侧边栏" onClick={
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: false })
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: true })
|
||||
} />:
|
||||
<Button icon={<IconIndentLeft />} theme="light" aria-label="关闭侧边栏" onClick={
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: true })
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: false })
|
||||
} />
|
||||
}
|
||||
</>
|
||||
|
||||
@@ -19,11 +19,11 @@ const PageLayout = () => {
|
||||
</Header>
|
||||
<Layout style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<Sider>
|
||||
{styleState.showSider ? null : <SiderBar />}
|
||||
{styleState.showSider ? <SiderBar /> : null}
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content
|
||||
style={{ overflowY: styleState.shouldInnerPadding?'hidden':'auto', padding: styleState.shouldInnerPadding? '0': '24px' }}
|
||||
style={{ overflowY: styleState.shouldInnerPadding?'auto':'hidden', padding: styleState.shouldInnerPadding? '24px': '0' }}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
|
||||
@@ -73,12 +73,6 @@ const SiderBar = () => {
|
||||
to: '/playground',
|
||||
icon: <IconCommentStroked />,
|
||||
},
|
||||
{
|
||||
text: '模型价格',
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
icon: <IconPriceTag />,
|
||||
},
|
||||
{
|
||||
text: '渠道',
|
||||
itemKey: 'channel',
|
||||
@@ -102,6 +96,16 @@ const SiderBar = () => {
|
||||
to: '/token',
|
||||
icon: <IconKey />,
|
||||
},
|
||||
{
|
||||
text: '数据看板',
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
className:
|
||||
localStorage.getItem('enable_data_export') === 'true'
|
||||
? 'semi-navigation-item-normal'
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '兑换码',
|
||||
itemKey: 'redemption',
|
||||
@@ -128,16 +132,6 @@ const SiderBar = () => {
|
||||
to: '/log',
|
||||
icon: <IconHistogram />,
|
||||
},
|
||||
{
|
||||
text: '数据看板',
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
className:
|
||||
localStorage.getItem('enable_data_export') === 'true'
|
||||
? 'semi-navigation-item-normal'
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '绘图',
|
||||
itemKey: 'midjourney',
|
||||
@@ -280,9 +274,9 @@ const SiderBar = () => {
|
||||
items={headerButtons}
|
||||
onSelect={(key) => {
|
||||
if (key.itemKey.toString().startsWith('chat')) {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
|
||||
@@ -44,6 +44,25 @@ export const StyleProvider = ({ children }) => {
|
||||
|
||||
updateIsMobile();
|
||||
|
||||
const updateShowSider = () => {
|
||||
// check pathname
|
||||
const pathname = window.location.pathname;
|
||||
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
dispatch({ type: 'SET_SIDER', payload: true });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
|
||||
if (isMobile()) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
updateShowSider()
|
||||
|
||||
|
||||
// Optionally, add event listeners to handle window resize
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
|
||||
|
||||
@@ -268,6 +268,44 @@ const colors = [
|
||||
'yellow',
|
||||
];
|
||||
|
||||
// 基础10色色板 (N ≤ 10)
|
||||
const baseColors = [
|
||||
'#1664FF', // 主色
|
||||
'#1AC6FF',
|
||||
'#FF8A00',
|
||||
'#3CC780',
|
||||
'#7442D4',
|
||||
'#FFC400',
|
||||
'#304D77',
|
||||
'#B48DEB',
|
||||
'#009488',
|
||||
'#FF7DDA'
|
||||
];
|
||||
|
||||
// 扩展20色色板 (10 < N ≤ 20)
|
||||
const extendedColors = [
|
||||
'#1664FF',
|
||||
'#B2CFFF',
|
||||
'#1AC6FF',
|
||||
'#94EFFF',
|
||||
'#FF8A00',
|
||||
'#FFCE7A',
|
||||
'#3CC780',
|
||||
'#B9EDCD',
|
||||
'#7442D4',
|
||||
'#DDC5FA',
|
||||
'#FFC400',
|
||||
'#FAE878',
|
||||
'#304D77',
|
||||
'#8B959E',
|
||||
'#B48DEB',
|
||||
'#EFE3FF',
|
||||
'#009488',
|
||||
'#59BAA8',
|
||||
'#FF7DDA',
|
||||
'#FFCFEE'
|
||||
];
|
||||
|
||||
export const modelColorMap = {
|
||||
'dall-e': 'rgb(147,112,219)', // 深紫色
|
||||
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
||||
@@ -312,14 +350,33 @@ export const modelColorMap = {
|
||||
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
|
||||
};
|
||||
|
||||
export function modelToColor(modelName) {
|
||||
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
|
||||
if (modelColorMap[modelName]) {
|
||||
return modelColorMap[modelName];
|
||||
}
|
||||
|
||||
// 2. 生成一个稳定的数字作为索引
|
||||
let hash = 0;
|
||||
for (let i = 0; i < modelName.length; i++) {
|
||||
hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// 3. 根据模型名称长度选择不同的色板
|
||||
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
|
||||
|
||||
// 4. 使用hash值选择颜色
|
||||
const index = hash % colorPalette.length;
|
||||
return colorPalette[index];
|
||||
}
|
||||
|
||||
export function stringToColor(str) {
|
||||
let sum = 0;
|
||||
// 对字符串中的每个字符进行操作
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
// 将字符的ASCII值加到sum中
|
||||
sum += str.charCodeAt(i);
|
||||
}
|
||||
// 使用模运算得到个位数
|
||||
let i = sum % colors.length;
|
||||
return colors[i];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,21 @@ body {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.semi-descriptions-double-small .semi-descriptions-item {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.panel-desc-card {
|
||||
/*min-width: 320px;*/
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.semi-table-tbody,
|
||||
.semi-table-row,
|
||||
.semi-table-row-cell {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
||||
|
||||
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
|
||||
import VChart from '@visactor/vchart';
|
||||
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import { VChart } from "@visactor/react-vchart";
|
||||
import {
|
||||
API,
|
||||
isAdmin,
|
||||
@@ -17,11 +17,16 @@ import {
|
||||
renderQuota,
|
||||
renderQuotaNumberWithDigit,
|
||||
stringToColor,
|
||||
modelToColor,
|
||||
} from '../../helpers/render';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
|
||||
const Detail = (props) => {
|
||||
const formRef = useRef();
|
||||
let now = new Date();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
@@ -40,32 +45,76 @@ const Detail = (props) => {
|
||||
inputs;
|
||||
const isAdminUser = isAdmin();
|
||||
const initialized = useRef(false);
|
||||
const [modelDataChart, setModelDataChart] = useState(null);
|
||||
const [modelDataPieChart, setModelDataPieChart] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [quotaData, setQuotaData] = useState([]);
|
||||
const [consumeQuota, setConsumeQuota] = useState(0);
|
||||
const [consumeTokens, setConsumeTokens] = useState(0);
|
||||
const [times, setTimes] = useState(0);
|
||||
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
|
||||
localStorage.getItem('data_export_default_time') || 'hour',
|
||||
);
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
if (name === 'data_export_default_time') {
|
||||
setDataExportDefaultTime(value);
|
||||
return;
|
||||
}
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const spec_line = {
|
||||
type: 'bar',
|
||||
data: [
|
||||
{
|
||||
id: 'barData',
|
||||
values: [],
|
||||
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
||||
const [lineData, setLineData] = useState([]);
|
||||
const [spec_pie, setSpecPie] = useState({
|
||||
type: 'pie',
|
||||
data: [{
|
||||
id: 'id0',
|
||||
values: pieData
|
||||
}],
|
||||
outerRadius: 0.8,
|
||||
innerRadius: 0.5,
|
||||
padAngle: 0.6,
|
||||
valueField: 'value',
|
||||
categoryField: 'type',
|
||||
pie: {
|
||||
style: {
|
||||
cornerRadius: 10,
|
||||
},
|
||||
],
|
||||
state: {
|
||||
hover: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
selected: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型调用次数占比',
|
||||
subtext: `总计:${renderNumber(times)}`,
|
||||
},
|
||||
legends: {
|
||||
visible: true,
|
||||
orient: 'left',
|
||||
},
|
||||
label: {
|
||||
visible: true,
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['type'],
|
||||
value: (datum) => renderNumber(datum['value']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
const [spec_line, setSpecLine] = useState({
|
||||
type: 'bar',
|
||||
data: [{
|
||||
id: 'barData',
|
||||
values: lineData
|
||||
}],
|
||||
xField: 'Time',
|
||||
yField: 'Usage',
|
||||
seriesField: 'Model',
|
||||
@@ -77,7 +126,7 @@ const Detail = (props) => {
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型消耗分布',
|
||||
subtext: '0',
|
||||
subtext: `总计:${renderQuota(consumeQuota, 2)}`,
|
||||
},
|
||||
bar: {
|
||||
// The state style of bar
|
||||
@@ -129,196 +178,197 @@ const Detail = (props) => {
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
|
||||
// 添加一个新的状态来存储模型-颜色映射
|
||||
const [modelColors, setModelColors] = useState({});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
if (name === 'data_export_default_time') {
|
||||
setDataExportDefaultTime(value);
|
||||
return;
|
||||
}
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const spec_pie = {
|
||||
type: 'pie',
|
||||
data: [
|
||||
{
|
||||
id: 'id0',
|
||||
values: [{ type: 'null', value: '0' }],
|
||||
},
|
||||
],
|
||||
outerRadius: 0.8,
|
||||
innerRadius: 0.5,
|
||||
padAngle: 0.6,
|
||||
valueField: 'value',
|
||||
categoryField: 'type',
|
||||
pie: {
|
||||
style: {
|
||||
cornerRadius: 10,
|
||||
},
|
||||
state: {
|
||||
hover: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
selected: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型调用次数占比',
|
||||
},
|
||||
legends: {
|
||||
visible: true,
|
||||
orient: 'left',
|
||||
},
|
||||
label: {
|
||||
visible: true,
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['type'],
|
||||
value: (datum) => renderNumber(datum['value']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
};
|
||||
|
||||
const loadQuotaData = async (lineChart, pieChart) => {
|
||||
const loadQuotaData = async () => {
|
||||
setLoading(true);
|
||||
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
} else {
|
||||
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setQuotaData(data);
|
||||
if (data.length === 0) {
|
||||
data.push({
|
||||
count: 0,
|
||||
model_name: '无数据',
|
||||
quota: 0,
|
||||
created_at: now.getTime() / 1000,
|
||||
try {
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
} else {
|
||||
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setQuotaData(data);
|
||||
if (data.length === 0) {
|
||||
data.push({
|
||||
count: 0,
|
||||
model_name: '无数据',
|
||||
quota: 0,
|
||||
created_at: now.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChartData(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChart(lineChart, pieChart, data);
|
||||
} else {
|
||||
showError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
await loadQuotaData(modelDataChart, modelDataPieChart);
|
||||
await loadQuotaData();
|
||||
};
|
||||
|
||||
const initChart = async () => {
|
||||
let lineChart = modelDataChart;
|
||||
if (!modelDataChart) {
|
||||
lineChart = new VChart(spec_line, { dom: 'model_data' });
|
||||
setModelDataChart(lineChart);
|
||||
lineChart.renderAsync();
|
||||
}
|
||||
let pieChart = modelDataPieChart;
|
||||
if (!modelDataPieChart) {
|
||||
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
|
||||
setModelDataPieChart(pieChart);
|
||||
pieChart.renderAsync();
|
||||
}
|
||||
console.log('init vchart');
|
||||
await loadQuotaData(lineChart, pieChart);
|
||||
await loadQuotaData();
|
||||
};
|
||||
|
||||
const updateChart = (lineChart, pieChart, data) => {
|
||||
if (isAdminUser) {
|
||||
// 将所有用户合并
|
||||
}
|
||||
let pieData = [];
|
||||
let lineData = [];
|
||||
let consumeQuota = 0;
|
||||
let times = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
consumeQuota += item.quota;
|
||||
times += item.count;
|
||||
// 合并model_name
|
||||
let pieItem = pieData.find((it) => it.type === item.model_name);
|
||||
const updateChartData = (data) => {
|
||||
let newPieData = [];
|
||||
let newLineData = [];
|
||||
let totalQuota = 0;
|
||||
let totalTimes = 0;
|
||||
let uniqueModels = new Set();
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称和时间点
|
||||
let uniqueTimes = new Set();
|
||||
data.forEach(item => {
|
||||
uniqueModels.add(item.model_name);
|
||||
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
|
||||
totalTokens += item.token_used;
|
||||
});
|
||||
|
||||
// 处理颜色映射
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
newModelColors[modelName] = modelColorMap[modelName] ||
|
||||
modelColors[modelName] ||
|
||||
modelToColor(modelName);
|
||||
});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
// 处理饼图数据
|
||||
for (let item of data) {
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
|
||||
let pieItem = newPieData.find((it) => it.type === item.model_name);
|
||||
if (pieItem) {
|
||||
pieItem.value += item.count;
|
||||
} else {
|
||||
pieData.push({
|
||||
newPieData.push({
|
||||
type: item.model_name,
|
||||
value: item.count,
|
||||
});
|
||||
}
|
||||
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
|
||||
// 转换日期格式
|
||||
let createTime = timestamp2string1(
|
||||
item.created_at,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
let lineItem = lineData.find(
|
||||
(it) => it.Time === createTime && it.Model === item.model_name,
|
||||
);
|
||||
if (lineItem) {
|
||||
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
|
||||
} else {
|
||||
lineData.push({
|
||||
Time: createTime,
|
||||
Model: item.model_name,
|
||||
Usage: parseFloat(getQuotaWithUnit(item.quota)),
|
||||
});
|
||||
}
|
||||
}
|
||||
setConsumeQuota(consumeQuota);
|
||||
setTimes(times);
|
||||
|
||||
// sort by count
|
||||
pieData.sort((a, b) => b.value - a.value);
|
||||
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
|
||||
spec_pie.data[0].values = pieData;
|
||||
// 处理柱状图数据
|
||||
let timePoints = Array.from(uniqueTimes);
|
||||
if (timePoints.length < 7) {
|
||||
// 根据时间粒度生成合适的时间点
|
||||
const generateTimePoints = () => {
|
||||
let lastTime = Math.max(...data.map(item => item.created_at));
|
||||
let points = [];
|
||||
let interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
: dataExportDefaultTime === 'day' ? 86400
|
||||
: 604800;
|
||||
|
||||
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
|
||||
spec_line.data[0].values = lineData;
|
||||
pieChart.updateSpec(spec_pie);
|
||||
lineChart.updateSpec(spec_line);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
|
||||
}
|
||||
return points.reverse();
|
||||
};
|
||||
|
||||
// pieChart.updateData('id0', pieData);
|
||||
// lineChart.updateData('barData', lineData);
|
||||
pieChart.reLayout();
|
||||
lineChart.reLayout();
|
||||
timePoints = generateTimePoints();
|
||||
}
|
||||
|
||||
// 为每个时间点和模型生成数据
|
||||
timePoints.forEach(time => {
|
||||
Array.from(uniqueModels).forEach(model => {
|
||||
let existingData = data.find(item =>
|
||||
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
|
||||
item.model_name === model
|
||||
);
|
||||
|
||||
newLineData.push({
|
||||
Time: time,
|
||||
Model: model,
|
||||
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 排序
|
||||
newPieData.sort((a, b) => b.value - a.value);
|
||||
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// 更新图表配置和数据
|
||||
setSpecPie(prev => ({
|
||||
...prev,
|
||||
data: [{ id: 'id0', values: newPieData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `总计:${renderNumber(totalTimes)}`
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
}
|
||||
}));
|
||||
|
||||
setSpecLine(prev => ({
|
||||
...prev,
|
||||
data: [{ id: 'barData', values: newLineData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `总计:${renderQuota(totalQuota, 2)}`
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
}
|
||||
}));
|
||||
|
||||
setPieData(newPieData);
|
||||
setLineData(newLineData);
|
||||
setConsumeQuota(totalQuota);
|
||||
setTimes(totalTimes);
|
||||
setConsumeTokens(totalTokens);
|
||||
};
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
userDispatch({type: 'login', payload: data});
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
|
||||
// if (dataExportDefaultTime === 'day') {
|
||||
// // 设置开始时间为7天前
|
||||
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
|
||||
// inputs.start_timestamp = st;
|
||||
// formRef.current.formApi.setValue('start_timestamp', st);
|
||||
// }
|
||||
getUserData()
|
||||
if (!initialized.current) {
|
||||
initVChartSemiTheme({
|
||||
isWatchingThemeSwitch: true,
|
||||
@@ -389,33 +439,93 @@ const Detail = (props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
label='查询'
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Form.Section>
|
||||
<Button
|
||||
label='查询'
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</>
|
||||
</Form>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ height: 500 }}>
|
||||
<div
|
||||
id='model_pie'
|
||||
style={{ width: '100%', minWidth: 100 }}
|
||||
></div>
|
||||
</div>
|
||||
<div style={{ height: 500 }}>
|
||||
<div
|
||||
id='model_data'
|
||||
style={{ width: '100%', minWidth: 100 }}
|
||||
></div>
|
||||
</div>
|
||||
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Card className='panel-desc-card'>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey='当前余额'>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='历史消耗'>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='请求次数'>
|
||||
{userState.user?.request_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Card>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey='统计额度'>
|
||||
{renderQuota(consumeQuota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='统计Tokens'>
|
||||
{consumeTokens}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='统计次数'>
|
||||
{times}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={styleState.isMobile ? 24 : 8}>
|
||||
<Card>
|
||||
<Descriptions row size='small'>
|
||||
<Descriptions.Item itemKey='平均RPM'>
|
||||
{(times /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='平均TPM'>
|
||||
{(consumeTokens /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card style={{marginTop: 20}}>
|
||||
<Tabs type="line" defaultActiveKey="1">
|
||||
<Tabs.TabPane tab="消耗分布" itemKey="1">
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={{ mode: "desktop-browser" }}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="调用次数分布" itemKey="2">
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={{ mode: "desktop-browser" }}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Spin>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
||||
@@ -326,7 +326,12 @@ const Playground = () => {
|
||||
}}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups}
|
||||
optionList={groups.map((group) => ({
|
||||
...group,
|
||||
label: styleState.isMobile && group.label.length > 16
|
||||
? group.label.substring(0, 16) + '...'
|
||||
: group.label,
|
||||
}))}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
|
||||
Reference in New Issue
Block a user