feat: able to query test log

This commit is contained in:
JustSong 2025-01-31 21:23:12 +08:00
parent 4f68f3e1b3
commit fa2a772731
6 changed files with 377 additions and 184 deletions

View File

@ -410,6 +410,7 @@ graph LR
27. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 27. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。
28. `INITIAL_ROOT_ACCESS_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量的 root 用户创建系统管理令牌。 28. `INITIAL_ROOT_ACCESS_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量的 root 用户创建系统管理令牌。
29. `ENFORCE_INCLUDE_USAGE`:是否强制在 stream 模型下返回 usage默认不开启可选值为 `true``false` 29. `ENFORCE_INCLUDE_USAGE`:是否强制在 stream 模型下返回 usage默认不开启可选值为 `true``false`
30. `TEST_PROMPT`:测试模型时的用户 prompt默认为 `Print your model name exactly and do not output without any other text.`
### 命令行参数 ### 命令行参数
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000` 1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`

View File

@ -1,13 +1,14 @@
package config package config
import ( import (
"github.com/songquanpeng/one-api/common/env"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/songquanpeng/one-api/common/env"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -162,3 +163,4 @@ var UserContentRequestProxy = env.String("USER_CONTENT_REQUEST_PROXY", "")
var UserContentRequestTimeout = env.Int("USER_CONTENT_REQUEST_TIMEOUT", 30) var UserContentRequestTimeout = env.Int("USER_CONTENT_REQUEST_TIMEOUT", 30)
var EnforceIncludeUsage = env.Bool("ENFORCE_INCLUDE_USAGE", false) var EnforceIncludeUsage = env.Bool("ENFORCE_INCLUDE_USAGE", false)
var TestPrompt = env.String("TEST_PROMPT", "Print your model name exactly and do not output without any other text.")

View File

@ -2,6 +2,7 @@ package controller
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -15,14 +16,17 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/ctxkey"
"github.com/songquanpeng/one-api/common/helper"
"github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/common/message" "github.com/songquanpeng/one-api/common/message"
"github.com/songquanpeng/one-api/middleware" "github.com/songquanpeng/one-api/middleware"
"github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/model"
"github.com/songquanpeng/one-api/monitor" "github.com/songquanpeng/one-api/monitor"
relay "github.com/songquanpeng/one-api/relay" "github.com/songquanpeng/one-api/relay"
"github.com/songquanpeng/one-api/relay/adaptor/openai"
"github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/channeltype"
"github.com/songquanpeng/one-api/relay/controller" "github.com/songquanpeng/one-api/relay/controller"
"github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/meta"
@ -35,18 +39,34 @@ func buildTestRequest(model string) *relaymodel.GeneralOpenAIRequest {
model = "gpt-3.5-turbo" model = "gpt-3.5-turbo"
} }
testRequest := &relaymodel.GeneralOpenAIRequest{ testRequest := &relaymodel.GeneralOpenAIRequest{
MaxTokens: 2, Model: model,
Model: model,
} }
testMessage := relaymodel.Message{ testMessage := relaymodel.Message{
Role: "user", Role: "user",
Content: "hi", Content: config.TestPrompt,
} }
testRequest.Messages = append(testRequest.Messages, testMessage) testRequest.Messages = append(testRequest.Messages, testMessage)
return testRequest return testRequest
} }
func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIRequest) (err error, openaiErr *relaymodel.Error) { func parseTestResponse(resp string) (*openai.TextResponse, string, error) {
var response openai.TextResponse
err := json.Unmarshal([]byte(resp), &response)
if err != nil {
return nil, "", err
}
if len(response.Choices) == 0 {
return nil, "", errors.New("response has no choices")
}
stringContent, ok := response.Choices[0].Content.(string)
if !ok {
return nil, "", errors.New("response content is not string")
}
return &response, stringContent, nil
}
func testChannel(ctx context.Context, channel *model.Channel, request *relaymodel.GeneralOpenAIRequest) (responseMessage string, err error, openaiErr *relaymodel.Error) {
startTime := time.Now()
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w) c, _ := gin.CreateTestContext(w)
c.Request = &http.Request{ c.Request = &http.Request{
@ -66,7 +86,7 @@ func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIReques
apiType := channeltype.ToAPIType(channel.Type) apiType := channeltype.ToAPIType(channel.Type)
adaptor := relay.GetAdaptor(apiType) adaptor := relay.GetAdaptor(apiType)
if adaptor == nil { if adaptor == nil {
return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil return "", fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
} }
adaptor.Init(meta) adaptor.Init(meta)
modelName := request.Model modelName := request.Model
@ -84,41 +104,69 @@ func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIReques
request.Model = modelName request.Model = modelName
convertedRequest, err := adaptor.ConvertRequest(c, relaymode.ChatCompletions, request) convertedRequest, err := adaptor.ConvertRequest(c, relaymode.ChatCompletions, request)
if err != nil { if err != nil {
return err, nil return "", err, nil
} }
jsonData, err := json.Marshal(convertedRequest) jsonData, err := json.Marshal(convertedRequest)
if err != nil { if err != nil {
return err, nil return "", err, nil
} }
defer func() {
logContent := fmt.Sprintf("渠道 %s 测试成功,响应:%s", channel.Name, responseMessage)
if err != nil || openaiErr != nil {
errorMessage := ""
if err != nil {
errorMessage = err.Error()
} else {
errorMessage = openaiErr.Message
}
logContent = fmt.Sprintf("渠道 %s 测试失败,错误:%s", channel.Name, errorMessage)
}
go model.RecordTestLog(ctx, &model.Log{
ChannelId: channel.Id,
ModelName: modelName,
Content: logContent,
ElapsedTime: helper.CalcElapsedTime(startTime),
})
}()
logger.SysLog(string(jsonData)) logger.SysLog(string(jsonData))
requestBody := bytes.NewBuffer(jsonData) requestBody := bytes.NewBuffer(jsonData)
c.Request.Body = io.NopCloser(requestBody) c.Request.Body = io.NopCloser(requestBody)
resp, err := adaptor.DoRequest(c, meta, requestBody) resp, err := adaptor.DoRequest(c, meta, requestBody)
if err != nil { if err != nil {
return err, nil return "", err, nil
} }
if resp != nil && resp.StatusCode != http.StatusOK { if resp != nil && resp.StatusCode != http.StatusOK {
err := controller.RelayErrorHandler(resp) err := controller.RelayErrorHandler(resp)
return fmt.Errorf("status code %d: %s", resp.StatusCode, err.Error.Message), &err.Error errorMessage := err.Error.Message
if errorMessage != "" {
errorMessage = ", error message: " + errorMessage
}
return "", fmt.Errorf("http status code: %d%s", resp.StatusCode, errorMessage), &err.Error
} }
usage, respErr := adaptor.DoResponse(c, resp, meta) usage, respErr := adaptor.DoResponse(c, resp, meta)
if respErr != nil { if respErr != nil {
return fmt.Errorf("%s", respErr.Error.Message), &respErr.Error return "", fmt.Errorf("%s", respErr.Error.Message), &respErr.Error
} }
if usage == nil { if usage == nil {
return errors.New("usage is nil"), nil return "", errors.New("usage is nil"), nil
}
rawResponse := w.Body.String()
_, responseMessage, err = parseTestResponse(rawResponse)
if err != nil {
return "", err, nil
} }
result := w.Result() result := w.Result()
// print result.Body // print result.Body
respBody, err := io.ReadAll(result.Body) respBody, err := io.ReadAll(result.Body)
if err != nil { if err != nil {
return err, nil return "", err, nil
} }
logger.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody))) logger.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
return nil, nil return responseMessage, nil, nil
} }
func TestChannel(c *gin.Context) { func TestChannel(c *gin.Context) {
ctx := c.Request.Context()
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@ -135,10 +183,10 @@ func TestChannel(c *gin.Context) {
}) })
return return
} }
model := c.Query("model") modelName := c.Query("model")
testRequest := buildTestRequest(model) testRequest := buildTestRequest(modelName)
tik := time.Now() tik := time.Now()
err, _ = testChannel(channel, testRequest) responseMessage, err, _ := testChannel(ctx, channel, testRequest)
tok := time.Now() tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds() milliseconds := tok.Sub(tik).Milliseconds()
if err != nil { if err != nil {
@ -148,18 +196,18 @@ func TestChannel(c *gin.Context) {
consumedTime := float64(milliseconds) / 1000.0 consumedTime := float64(milliseconds) / 1000.0
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": err.Error(), "message": err.Error(),
"time": consumedTime, "time": consumedTime,
"model": model, "modelName": modelName,
}) })
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": responseMessage,
"time": consumedTime, "time": consumedTime,
"model": model, "modelName": modelName,
}) })
return return
} }
@ -167,7 +215,7 @@ func TestChannel(c *gin.Context) {
var testAllChannelsLock sync.Mutex var testAllChannelsLock sync.Mutex
var testAllChannelsRunning bool = false var testAllChannelsRunning bool = false
func testChannels(notify bool, scope string) error { func testChannels(ctx context.Context, notify bool, scope string) error {
if config.RootUserEmail == "" { if config.RootUserEmail == "" {
config.RootUserEmail = model.GetRootUserEmail() config.RootUserEmail = model.GetRootUserEmail()
} }
@ -191,7 +239,7 @@ func testChannels(notify bool, scope string) error {
isChannelEnabled := channel.Status == model.ChannelStatusEnabled isChannelEnabled := channel.Status == model.ChannelStatusEnabled
tik := time.Now() tik := time.Now()
testRequest := buildTestRequest("") testRequest := buildTestRequest("")
err, openaiErr := testChannel(channel, testRequest) _, err, openaiErr := testChannel(ctx, channel, testRequest)
tok := time.Now() tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds() milliseconds := tok.Sub(tik).Milliseconds()
if isChannelEnabled && milliseconds > disableThreshold { if isChannelEnabled && milliseconds > disableThreshold {
@ -225,11 +273,12 @@ func testChannels(notify bool, scope string) error {
} }
func TestChannels(c *gin.Context) { func TestChannels(c *gin.Context) {
ctx := c.Request.Context()
scope := c.Query("scope") scope := c.Query("scope")
if scope == "" { if scope == "" {
scope = "all" scope = "all"
} }
err := testChannels(true, scope) err := testChannels(ctx, true, scope)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
@ -245,10 +294,11 @@ func TestChannels(c *gin.Context) {
} }
func AutomaticallyTestChannels(frequency int) { func AutomaticallyTestChannels(frequency int) {
ctx := context.Background()
for { for {
time.Sleep(time.Duration(frequency) * time.Minute) time.Sleep(time.Duration(frequency) * time.Minute)
logger.SysLog("testing all channels") logger.SysLog("testing all channels")
_ = testChannels(false, "all") _ = testChannels(ctx, false, "all")
logger.SysLog("channel test finished") logger.SysLog("channel test finished")
} }
} }

View File

@ -37,6 +37,7 @@ const (
LogTypeConsume LogTypeConsume
LogTypeManage LogTypeManage
LogTypeSystem LogTypeSystem
LogTypeTest
) )
func recordLogHelper(ctx context.Context, log *Log) { func recordLogHelper(ctx context.Context, log *Log) {
@ -86,6 +87,12 @@ func RecordConsumeLog(ctx context.Context, log *Log) {
recordLogHelper(ctx, log) recordLogHelper(ctx, log)
} }
func RecordTestLog(ctx context.Context, log *Log) {
log.CreatedAt = helper.GetTimestamp()
log.Type = LogTypeTest
recordLogHelper(ctx, log)
}
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) { func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) {
var tx *gorm.DB var tx *gorm.DB
if logType == LogTypeUnknown { if logType == LogTypeUnknown {

View File

@ -1,5 +1,15 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Dropdown, Form, Input, Label, Message, Pagination, Popup, Table } from 'semantic-ui-react'; import {
Button,
Dropdown,
Form,
Input,
Label,
Message,
Pagination,
Popup,
Table,
} from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
API, API,
@ -9,31 +19,31 @@ import {
showError, showError,
showInfo, showInfo,
showSuccess, showSuccess,
timestamp2string timestamp2string,
} from '../helpers'; } from '../helpers';
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber } from '../helpers/render'; import { renderGroup, renderNumber } from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return <>{timestamp2string(timestamp)}</>;
<>
{timestamp2string(timestamp)}
</>
);
} }
let type2label = undefined; let type2label = undefined;
function renderType(type) { function renderType(type) {
if (!type2label) { if (!type2label) {
type2label = new Map; type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
} }
type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
} }
return <Label basic color={type2label[type]?.color}>{type2label[type] ? type2label[type].text : type}</Label>; return (
<Label basic color={type2label[type]?.color}>
{type2label[type] ? type2label[type].text : type}
</Label>
);
} }
function renderBalance(type, balance) { function renderBalance(type, balance) {
@ -62,10 +72,10 @@ function renderBalance(type, balance) {
} }
function isShowDetail() { function isShowDetail() {
return localStorage.getItem("show_detail") === "true"; return localStorage.getItem('show_detail') === 'true';
} }
const promptID = "detail" const promptID = 'detail';
const ChannelsTable = () => { const ChannelsTable = () => {
const [channels, setChannels] = useState([]); const [channels, setChannels] = useState([]);
@ -81,33 +91,37 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/?p=${startIdx}`); const res = await API.get(`/api/channel/?p=${startIdx}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
let localChannels = data.map((channel) => { let localChannels = data.map((channel) => {
if (channel.models === '') { if (channel.models === '') {
channel.models = []; channel.models = [];
channel.test_model = ""; channel.test_model = '';
} else {
channel.models = channel.models.split(',');
if (channel.models.length > 0) {
channel.test_model = channel.models[0];
}
channel.model_options = channel.models.map((model) => {
return {
key: model,
text: model,
value: model,
}
})
console.log('channel', channel)
}
return channel;
});
if (startIdx === 0) {
setChannels(localChannels);
} else { } else {
let newChannels = [...channels]; channel.models = channel.models.split(',');
newChannels.splice(startIdx * ITEMS_PER_PAGE, data.length, ...localChannels); if (channel.models.length > 0) {
setChannels(newChannels); channel.test_model = channel.models[0];
}
channel.model_options = channel.models.map((model) => {
return {
key: model,
text: model,
value: model,
};
});
console.log('channel', channel);
} }
return channel;
});
if (startIdx === 0) {
setChannels(localChannels);
} else {
let newChannels = [...channels];
newChannels.splice(
startIdx * ITEMS_PER_PAGE,
data.length,
...localChannels
);
setChannels(newChannels);
}
} else { } else {
showError(message); showError(message);
} }
@ -131,8 +145,8 @@ const ChannelsTable = () => {
const toggleShowDetail = () => { const toggleShowDetail = () => {
setShowDetail(!showDetail); setShowDetail(!showDetail);
localStorage.setItem("show_detail", (!showDetail).toString()); localStorage.setItem('show_detail', (!showDetail).toString());
} };
useEffect(() => { useEffect(() => {
loadChannels(0) loadChannels(0)
@ -196,13 +210,19 @@ const ChannelsTable = () => {
const renderStatus = (status) => { const renderStatus = (status) => {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>已启用</Label>; return (
<Label basic color='green'>
已启用
</Label>
);
case 2: case 2:
return ( return (
<Popup <Popup
trigger={<Label basic color='red'> trigger={
已禁用 <Label basic color='red'>
</Label>} 已禁用
</Label>
}
content='本渠道被手动禁用' content='本渠道被手动禁用'
basic basic
/> />
@ -210,9 +230,11 @@ const ChannelsTable = () => {
case 3: case 3:
return ( return (
<Popup <Popup
trigger={<Label basic color='yellow'> trigger={
已禁用 <Label basic color='yellow'>
</Label>} 已禁用
</Label>
}
content='本渠道被程序自动禁用' content='本渠道被程序自动禁用'
basic basic
/> />
@ -230,15 +252,35 @@ const ChannelsTable = () => {
let time = responseTime / 1000; let time = responseTime / 1000;
time = time.toFixed(2) + ' 秒'; time = time.toFixed(2) + ' 秒';
if (responseTime === 0) { if (responseTime === 0) {
return <Label basic color='grey'>未测试</Label>; return (
<Label basic color='grey'>
未测试
</Label>
);
} else if (responseTime <= 1000) { } else if (responseTime <= 1000) {
return <Label basic color='green'>{time}</Label>; return (
<Label basic color='green'>
{time}
</Label>
);
} else if (responseTime <= 3000) { } else if (responseTime <= 3000) {
return <Label basic color='olive'>{time}</Label>; return (
<Label basic color='olive'>
{time}
</Label>
);
} else if (responseTime <= 5000) { } else if (responseTime <= 5000) {
return <Label basic color='yellow'>{time}</Label>; return (
<Label basic color='yellow'>
{time}
</Label>
);
} else { } else {
return <Label basic color='red'>{time}</Label>; return (
<Label basic color='red'>
{time}
</Label>
);
} }
}; };
@ -277,7 +319,11 @@ const ChannelsTable = () => {
newChannels[realIdx].response_time = time * 1000; newChannels[realIdx].response_time = time * 1000;
newChannels[realIdx].test_time = Date.now() / 1000; newChannels[realIdx].test_time = Date.now() / 1000;
setChannels(newChannels); setChannels(newChannels);
showInfo(`渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(2)} 秒。`); showInfo(
`渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(
2
)} 模型输出${message}`
);
} else { } else {
showError(message); showError(message);
} }
@ -360,7 +406,6 @@ const ChannelsTable = () => {
setLoading(false); setLoading(false);
}; };
return ( return (
<> <>
<Form onSubmit={searchChannels}> <Form onSubmit={searchChannels}>
@ -374,20 +419,22 @@ const ChannelsTable = () => {
onChange={handleKeywordChange} onChange={handleKeywordChange}
/> />
</Form> </Form>
{ {showPrompt && (
showPrompt && ( <Message
<Message onDismiss={() => { onDismiss={() => {
setShowPrompt(false); setShowPrompt(false);
setPromptShown(promptID); setPromptShown(promptID);
}}> }}
OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为 0对于支持的渠道类型请点击余额进行刷新 >
<br/> OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为
渠道测试仅支持 chat 模型优先使用 gpt-3.5-turbo如果该模型不可用则使用你所配置的模型列表中的第一个模型 0对于支持的渠道类型请点击余额进行刷新
<br/> <br />
点击下方详情按钮可以显示余额以及设置额外的测试模型 渠道测试仅支持 chat 模型优先使用
</Message> gpt-3.5-turbo如果该模型不可用则使用你所配置的模型列表中的第一个模型
) <br />
} 点击下方详情按钮可以显示余额以及设置额外的测试模型
</Message>
)}
<Table basic compact size='small'> <Table basic compact size='small'>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
@ -478,7 +525,11 @@ const ChannelsTable = () => {
<Table.Cell>{renderStatus(channel.status)}</Table.Cell> <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
<Table.Cell> <Table.Cell>
<Popup <Popup
content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'} content={
channel.test_time
? renderTimestamp(channel.test_time)
: '未测试'
}
key={channel.id} key={channel.id}
trigger={renderResponseTime(channel.response_time)} trigger={renderResponseTime(channel.response_time)}
basic basic
@ -486,27 +537,38 @@ const ChannelsTable = () => {
</Table.Cell> </Table.Cell>
<Table.Cell hidden={!showDetail}> <Table.Cell hidden={!showDetail}>
<Popup <Popup
trigger={<span onClick={() => { trigger={
updateChannelBalance(channel.id, channel.name, idx); <span
}} style={{ cursor: 'pointer' }}> onClick={() => {
{renderBalance(channel.type, channel.balance)} updateChannelBalance(channel.id, channel.name, idx);
</span>} }}
style={{ cursor: 'pointer' }}
>
{renderBalance(channel.type, channel.balance)}
</span>
}
content='点击更新' content='点击更新'
basic basic
/> />
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>
<Popup <Popup
trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => { trigger={
manageChannel( <Input
channel.id, type='number'
'priority', defaultValue={channel.priority}
idx, onBlur={(event) => {
event.target.value manageChannel(
); channel.id,
}}> 'priority',
<input style={{ maxWidth: '60px' }} /> idx,
</Input>} event.target.value
);
}}
>
<input style={{ maxWidth: '60px' }} />
</Input>
}
content='渠道选择优先级,越高越优先' content='渠道选择优先级,越高越优先'
basic basic
/> />
@ -528,7 +590,12 @@ const ChannelsTable = () => {
size={'small'} size={'small'}
positive positive
onClick={() => { onClick={() => {
testChannel(channel.id, channel.name, idx, channel.test_model); testChannel(
channel.id,
channel.name,
idx,
channel.test_model
);
}} }}
> >
测试 测试
@ -590,14 +657,31 @@ const ChannelsTable = () => {
<Table.Footer> <Table.Footer>
<Table.Row> <Table.Row>
<Table.HeaderCell colSpan={showDetail ? "10" : "8"}> <Table.HeaderCell colSpan={showDetail ? '10' : '8'}>
<Button size='small' as={Link} to='/channel/add' loading={loading}> <Button
size='small'
as={Link}
to='/channel/add'
loading={loading}
>
添加新的渠道 添加新的渠道
</Button> </Button>
<Button size='small' loading={loading} onClick={()=>{testChannels("all")}}> <Button
size='small'
loading={loading}
onClick={() => {
testChannels('all');
}}
>
测试所有渠道 测试所有渠道
</Button> </Button>
<Button size='small' loading={loading} onClick={()=>{testChannels("disabled")}}> <Button
size='small'
loading={loading}
onClick={() => {
testChannels('disabled');
}}
>
测试禁用渠道 测试禁用渠道
</Button> </Button>
{/*<Button size='small' onClick={updateAllChannelsBalance}*/} {/*<Button size='small' onClick={updateAllChannelsBalance}*/}
@ -612,7 +696,12 @@ const ChannelsTable = () => {
flowing flowing
hoverable hoverable
> >
<Button size='small' loading={loading} negative onClick={deleteAllDisabledChannels}> <Button
size='small'
loading={loading}
negative
onClick={deleteAllDisabledChannels}
>
确认删除 确认删除
</Button> </Button>
</Popup> </Popup>
@ -627,8 +716,12 @@ const ChannelsTable = () => {
(channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0) (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
} }
/> />
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>
<Button size='small' onClick={toggleShowDetail}>{showDetail ? "隐藏详情" : "详情"}</Button> 刷新
</Button>
<Button size='small' onClick={toggleShowDetail}>
{showDetail ? '隐藏详情' : '详情'}
</Button>
</Table.HeaderCell> </Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Footer> </Table.Footer>

View File

@ -21,6 +21,7 @@ import {
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderColorLabel, renderQuota } from '../helpers/render'; import { renderColorLabel, renderQuota } from '../helpers/render';
import { Link } from 'react-router-dom';
function renderTimestamp(timestamp, request_id) { function renderTimestamp(timestamp, request_id) {
return ( return (
@ -50,6 +51,7 @@ const LOG_OPTIONS = [
{ key: '2', text: '消费', value: 2 }, { key: '2', text: '消费', value: 2 },
{ key: '3', text: '管理', value: 3 }, { key: '3', text: '管理', value: 3 },
{ key: '4', text: '系统', value: 4 }, { key: '4', text: '系统', value: 4 },
{ key: '5', text: '测试', value: 5 },
]; ];
function renderType(type) { function renderType(type) {
@ -78,6 +80,12 @@ function renderType(type) {
系统 系统
</Label> </Label>
); );
case 5:
return (
<Label basic color='violet'>
测试
</Label>
);
default: default:
return ( return (
<Label basic color='black'> <Label basic color='black'>
@ -203,6 +211,10 @@ const LogsTable = () => {
setShowStat(!showStat); setShowStat(!showStat);
}; };
const showUserTokenQuota = () => {
return logType !== 5;
};
const loadLogs = async (startIdx) => { const loadLogs = async (startIdx) => {
let url = ''; let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
@ -399,26 +411,6 @@ const LogsTable = () => {
渠道 渠道
</Table.HeaderCell> </Table.HeaderCell>
)} )}
{isAdminUser && (
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('username');
}}
width={1}
>
用户
</Table.HeaderCell>
)}
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('token_name');
}}
width={1}
>
令牌
</Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@ -437,33 +429,57 @@ const LogsTable = () => {
> >
模型 模型
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell {showUserTokenQuota() && (
style={{ cursor: 'pointer' }} <>
onClick={() => { {isAdminUser && (
sortLog('prompt_tokens'); <Table.HeaderCell
}} style={{ cursor: 'pointer' }}
width={1} onClick={() => {
> sortLog('username');
提示 }}
</Table.HeaderCell> width={1}
<Table.HeaderCell >
style={{ cursor: 'pointer' }} 用户
onClick={() => { </Table.HeaderCell>
sortLog('completion_tokens'); )}
}} <Table.HeaderCell
width={1} style={{ cursor: 'pointer' }}
> onClick={() => {
补全 sortLog('token_name');
</Table.HeaderCell> }}
<Table.HeaderCell width={1}
style={{ cursor: 'pointer' }} >
onClick={() => { 令牌
sortLog('quota'); </Table.HeaderCell>
}} <Table.HeaderCell
width={1} style={{ cursor: 'pointer' }}
> onClick={() => {
额度 sortLog('prompt_tokens');
</Table.HeaderCell> }}
width={1}
>
提示
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('completion_tokens');
}}
width={1}
>
补全
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('quota');
}}
width={1}
>
额度
</Table.HeaderCell>
</>
)}
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@ -491,34 +507,58 @@ const LogsTable = () => {
</Table.Cell> </Table.Cell>
{isAdminUser && ( {isAdminUser && (
<Table.Cell> <Table.Cell>
{log.channel ? <Label basic>{log.channel}</Label> : ''} {log.channel ? (
</Table.Cell> <Label
)} basic
{isAdminUser && ( as={Link}
<Table.Cell> to={`/channel/edit/${log.channel}`}
{log.username ? ( >
<Label basic>{log.username}</Label> {log.channel}
</Label>
) : ( ) : (
'' ''
)} )}
</Table.Cell> </Table.Cell>
)} )}
<Table.Cell>
{log.token_name ? renderColorLabel(log.token_name) : ''}
</Table.Cell>
<Table.Cell>{renderType(log.type)}</Table.Cell> <Table.Cell>{renderType(log.type)}</Table.Cell>
<Table.Cell> <Table.Cell>
{log.model_name ? renderColorLabel(log.model_name) : ''} {log.model_name ? renderColorLabel(log.model_name) : ''}
</Table.Cell> </Table.Cell>
<Table.Cell> {showUserTokenQuota() && (
{log.prompt_tokens ? log.prompt_tokens : ''} <>
</Table.Cell> {isAdminUser && (
<Table.Cell> <Table.Cell>
{log.completion_tokens ? log.completion_tokens : ''} {log.username ? (
</Table.Cell> <Label
<Table.Cell> basic
{log.quota ? renderQuota(log.quota, 6) : ''} as={Link}
</Table.Cell> to={`/user/edit/${log.user_id}`}
>
{log.username}
</Label>
) : (
''
)}
</Table.Cell>
)}
<Table.Cell>
{log.token_name
? renderColorLabel(log.token_name)
: ''}
</Table.Cell>
<Table.Cell>
{log.prompt_tokens ? log.prompt_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.completion_tokens ? log.completion_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.quota ? renderQuota(log.quota, 6) : ''}
</Table.Cell>
</>
)}
<Table.Cell>{renderDetail(log)}</Table.Cell> <Table.Cell>{renderDetail(log)}</Table.Cell>
</Table.Row> </Table.Row>
); );