Compare commits

...

18 Commits

Author SHA1 Message Date
1808837298@qq.com
1819c4d5f5 feat(error): Enhance error handling with optional detailed error messages 2025-03-11 17:25:06 +08:00
1808837298@qq.com
6f24dddcb2 feat(relay): Add pass-through request option for global settings 2025-03-11 17:02:35 +08:00
1808837298@qq.com
8de29fbb83 Merge remote-tracking branch 'origin/main' 2025-03-11 16:41:18 +08:00
Calcium-Ion
f2163acf2b Merge pull request #849 from OrdinarySF/main
feat(setting): add 'Document Link' option i18n support
2025-03-11 16:27:37 +08:00
Ordinary
5259acfacd feat(setting): add 'Document Link' option i18n support 2025-03-11 08:22:59 +00:00
1808837298@qq.com
3122b8a36a fix: Improve mobile layout and scrolling behavior 2025-03-11 15:05:23 +08:00
1808837298@qq.com
bbe7223a85 Merge remote-tracking branch 'origin/main' 2025-03-11 14:55:56 +08:00
1808837298@qq.com
2af05c166c feat: Improve route handling and dynamic chat navigation in SiderBar 2025-03-11 14:55:48 +08:00
Calcium-Ion
ecb5b5630c Merge pull request #845 from Sh1n3zZ/gemini-embedding
feat: gemini Embeddings support
2025-03-10 23:46:53 +08:00
Sh1n3zZ
e1b9f164f9 feat: gemini Embeddings support 2025-03-10 23:32:06 +08:00
1808837298@qq.com
69db1f1465 Merge remote-tracking branch 'origin/main' 2025-03-10 21:05:43 +08:00
1808837298@qq.com
94549f9687 refactor: Improve responsive design across multiple setting pages 2025-03-10 21:05:22 +08:00
Calcium-Ion
c7e1bab18a Merge pull request #842 from asjfoajs/dev
Fix: Under Ali's large model, the task ID result for image retrieval …
2025-03-10 20:18:53 +08:00
1808837298@qq.com
627f95b034 refactor: Remove unnecessary transition styles and simplify sidebar state management 2025-03-10 20:14:23 +08:00
1808837298@qq.com
8b99eec440 refactor: Improve sidebar state management and layout responsiveness 2025-03-10 19:48:17 +08:00
1808837298@qq.com
49bfd2b719 feat: Enhance mobile UI responsiveness and layout for ChannelsTable and SiderBar 2025-03-10 19:01:56 +08:00
霍雨佳
434e9d7695 Fix: Under Ali's large model, the task ID result for image retrieval is incorrect.
Reason: The URL is incomplete, missing baseurl.
Solution: Add baseurl. url := fmt.Sprintf("%s/api/v1/tasks/%s", info.BaseUrl, taskID).
2025-03-10 16:22:40 +08:00
1808837298@qq.com
b2938ffe2c refactor: Improve mobile responsiveness and scrolling behavior in UI layout 2025-03-10 15:49:32 +08:00
39 changed files with 714 additions and 599 deletions

View File

@@ -125,7 +125,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
err := service.RelayErrorHandler(httpResp)
err := service.RelayErrorHandler(httpResp, true)
return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err
}
}

View File

@@ -27,7 +27,7 @@ func oaiImage2Ali(request dto.ImageRequest) *AliImageRequest {
}
func updateTask(info *relaycommon.RelayInfo, taskID string, key string) (*AliResponse, error, []byte) {
url := fmt.Sprintf("/api/v1/tasks/%s", taskID)
url := fmt.Sprintf("%s/api/v1/tasks/%s", info.BaseUrl, taskID)
var aliResponse AliResponse

View File

@@ -70,6 +70,12 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
return fmt.Sprintf("%s/%s/models/%s:predict", info.BaseUrl, version, info.UpstreamModelName), nil
}
if strings.HasPrefix(info.UpstreamModelName, "text-embedding") ||
strings.HasPrefix(info.UpstreamModelName, "embedding") ||
strings.HasPrefix(info.UpstreamModelName, "gemini-embedding") {
return fmt.Sprintf("%s/%s/models/%s:embedContent", info.BaseUrl, version, info.UpstreamModelName), nil
}
action := "generateContent"
if info.IsStream {
action = "streamGenerateContent?alt=sse"
@@ -99,8 +105,37 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt
}
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
if request.Input == nil {
return nil, errors.New("input is required")
}
inputs := request.ParseInput()
if len(inputs) == 0 {
return nil, errors.New("input is empty")
}
// only process the first input
geminiRequest := GeminiEmbeddingRequest{
Content: GeminiChatContent{
Parts: []GeminiPart{
{
Text: inputs[0],
},
},
},
}
// set specific parameters for different models
// https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent
switch info.UpstreamModelName {
case "text-embedding-004":
// except embedding-001 supports setting `OutputDimensionality`
if request.Dimensions > 0 {
geminiRequest.OutputDimensionality = request.Dimensions
}
}
return geminiRequest, nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
@@ -112,6 +147,13 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
return GeminiImageHandler(c, resp, info)
}
// check if the model is an embedding model
if strings.HasPrefix(info.UpstreamModelName, "text-embedding") ||
strings.HasPrefix(info.UpstreamModelName, "embedding") ||
strings.HasPrefix(info.UpstreamModelName, "gemini-embedding") {
return GeminiEmbeddingHandler(c, resp, info)
}
if info.IsStream {
err, usage = GeminiChatStreamHandler(c, resp, info)
} else {

View File

@@ -18,6 +18,10 @@ var ModelList = []string{
"gemini-2.0-flash-thinking-exp",
// imagen models
"imagen-3.0-generate-002",
// embedding models
"gemini-embedding-exp-03-07",
"text-embedding-004",
"embedding-001",
}
var SafetySettingList = []string{

View File

@@ -136,3 +136,19 @@ type GeminiImagePrediction struct {
RaiFilteredReason string `json:"raiFilteredReason,omitempty"`
SafetyAttributes any `json:"safetyAttributes,omitempty"`
}
// Embedding related structs
type GeminiEmbeddingRequest struct {
Content GeminiChatContent `json:"content"`
TaskType string `json:"taskType,omitempty"`
Title string `json:"title,omitempty"`
OutputDimensionality int `json:"outputDimensionality,omitempty"`
}
type GeminiEmbeddingResponse struct {
Embedding ContentEmbedding `json:"embedding"`
}
type ContentEmbedding struct {
Values []float64 `json:"values"`
}

View File

@@ -580,3 +580,52 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
_, err = c.Writer.Write(jsonResponse)
return nil, &usage
}
func GeminiEmbeddingHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
responseBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return nil, service.OpenAIErrorWrapper(readErr, "read_response_body_failed", http.StatusInternalServerError)
}
_ = resp.Body.Close()
var geminiResponse GeminiEmbeddingResponse
if jsonErr := json.Unmarshal(responseBody, &geminiResponse); jsonErr != nil {
return nil, service.OpenAIErrorWrapper(jsonErr, "unmarshal_response_body_failed", http.StatusInternalServerError)
}
// convert to openai format response
openAIResponse := dto.OpenAIEmbeddingResponse{
Object: "list",
Data: []dto.OpenAIEmbeddingResponseItem{
{
Object: "embedding",
Embedding: geminiResponse.Embedding.Values,
Index: 0,
},
},
Model: info.UpstreamModelName,
}
// calculate usage
// https://ai.google.dev/gemini-api/docs/pricing?hl=zh-cn#text-embedding-004
// Google has not yet clarified how embedding models will be billed
// refer to openai billing method to use input tokens billing
// https://platform.openai.com/docs/guides/embeddings#what-are-embeddings
usage = &dto.Usage{
PromptTokens: info.PromptTokens,
CompletionTokens: 0,
TotalTokens: info.PromptTokens,
}
openAIResponse.Usage = *usage.(*dto.Usage)
jsonResponse, jsonErr := json.Marshal(openAIResponse)
if jsonErr != nil {
return nil, service.OpenAIErrorWrapper(jsonErr, "marshal_response_failed", http.StatusInternalServerError)
}
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(resp.StatusCode)
_, _ = c.Writer.Write(jsonResponse)
return usage, nil
}

View File

@@ -117,7 +117,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -155,7 +155,7 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
openaiErr := service.RelayErrorHandler(httpResp)
openaiErr := service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -17,6 +17,7 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting"
"one-api/setting/model_setting"
"strings"
"time"
@@ -152,38 +153,37 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
adaptor.Init(relayInfo)
var requestBody io.Reader
//if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped {
// body, err := common.GetRequestBody(c)
// if err != nil {
// return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
// }
// requestBody = bytes.NewBuffer(body)
//} else {
//
//}
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
if model_setting.GetGlobalSettings().PassThroughRequestEnabled {
body, err := common.GetRequestBody(c)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonData)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonData)
statusCodeMappingStr := c.GetString("status_code_mapping")
var httpResp *http.Response
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil {
return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
}
statusCodeMappingStr := c.GetString("status_code_mapping")
if resp != nil {
httpResp = resp.(*http.Response)
relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -98,7 +98,7 @@ func EmbeddingHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode)
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -90,7 +90,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp)
openaiErr = service.RelayErrorHandler(httpResp, false)
// reset status code 重置状态码
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr

View File

@@ -50,7 +50,7 @@ func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAI
return openaiErr
}
func RelayErrorHandler(resp *http.Response) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) {
func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) {
errWithStatusCode = &dto.OpenAIErrorWithStatusCode{
StatusCode: resp.StatusCode,
Error: dto.OpenAIError{
@@ -70,6 +70,11 @@ func RelayErrorHandler(resp *http.Response) (errWithStatusCode *dto.OpenAIErrorW
var errResponse dto.GeneralErrorResponse
err = json.Unmarshal(responseBody, &errResponse)
if err != nil {
if showBodyWhenFail {
errWithStatusCode.Error.Message = string(responseBody)
} else {
errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode)
}
return
}
if errResponse.Error.Message != "" {

View File

@@ -0,0 +1,26 @@
package model_setting
import (
"one-api/setting/config"
)
type GlobalSettings struct {
PassThroughRequestEnabled bool `json:"pass_through_request_enabled"`
}
// 默认配置
var defaultOpenaiSettings = GlobalSettings{
PassThroughRequestEnabled: false,
}
// 全局实例
var globalSettings = defaultOpenaiSettings
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("global", &globalSettings)
}
func GetGlobalSettings() *GlobalSettings {
return &globalSettings
}

View File

@@ -1,5 +1,5 @@
import React, { lazy, Suspense, useContext, useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useLocation } from 'react-router-dom';
import Loading from './components/Loading';
import User from './pages/User';
import { PrivateRoute } from './components/PrivateRoute';
@@ -8,10 +8,8 @@ import LoginForm from './components/LoginForm';
import NotFound from './pages/NotFound';
import Setting from './pages/Setting';
import EditUser from './pages/User/EditUser';
import { getLogo, getSystemName } from './helpers';
import PasswordResetForm from './components/PasswordResetForm';
import PasswordResetConfirm from './components/PasswordResetConfirm';
import { UserContext } from './context/User';
import Channel from './pages/Channel';
import Token from './pages/Token';
import EditChannel from './pages/Channel/EditChannel';
@@ -26,10 +24,6 @@ import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js";
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
import { useTranslation } from 'react-i18next';
import { StatusContext } from './context/Status';
import { setStatusData } from './helpers/data.js';
import { API, showError } from './helpers';
import PersonalSetting from './components/PersonalSetting.js';
const Home = lazy(() => import('./pages/Home'));
@@ -37,13 +31,15 @@ const Detail = lazy(() => import('./pages/Detail'));
const About = lazy(() => import('./pages/About'));
function App() {
const location = useLocation();
return (
<>
<Routes>
<Route
path='/'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Home />
</Suspense>
}
@@ -59,7 +55,7 @@ function App() {
<Route
path='/channel/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditChannel />
</Suspense>
}
@@ -67,7 +63,7 @@ function App() {
<Route
path='/channel/add'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditChannel />
</Suspense>
}
@@ -107,7 +103,7 @@ function App() {
<Route
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditUser />
</Suspense>
}
@@ -115,7 +111,7 @@ function App() {
<Route
path='/user/edit'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<EditUser />
</Suspense>
}
@@ -123,7 +119,7 @@ function App() {
<Route
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PasswordResetConfirm />
</Suspense>
}
@@ -131,7 +127,7 @@ function App() {
<Route
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<LoginForm />
</Suspense>
}
@@ -139,7 +135,7 @@ function App() {
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<RegisterForm />
</Suspense>
}
@@ -147,7 +143,7 @@ function App() {
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PasswordResetForm />
</Suspense>
}
@@ -155,7 +151,7 @@ function App() {
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='github'></OAuth2Callback>
</Suspense>
}
@@ -163,7 +159,7 @@ function App() {
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<OAuth2Callback type='linuxdo'></OAuth2Callback>
</Suspense>
}
@@ -172,7 +168,7 @@ function App() {
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Setting />
</Suspense>
</PrivateRoute>
@@ -182,7 +178,7 @@ function App() {
path='/personal'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PersonalSetting />
</Suspense>
</PrivateRoute>
@@ -192,7 +188,7 @@ function App() {
path='/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<TopUp />
</Suspense>
</PrivateRoute>
@@ -210,7 +206,7 @@ function App() {
path='/detail'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Detail />
</Suspense>
</PrivateRoute>
@@ -220,7 +216,7 @@ function App() {
path='/midjourney'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Midjourney />
</Suspense>
</PrivateRoute>
@@ -230,7 +226,7 @@ function App() {
path='/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Task />
</Suspense>
</PrivateRoute>
@@ -239,7 +235,7 @@ function App() {
<Route
path='/pricing'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Pricing />
</Suspense>
}
@@ -247,7 +243,7 @@ function App() {
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<About />
</Suspense>
}
@@ -255,7 +251,7 @@ function App() {
<Route
path='/chat/:id?'
element={
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat />
</Suspense>
}
@@ -265,7 +261,7 @@ function App() {
path='/chat2link'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Chat2Link />
</Suspense>
</PrivateRoute>

View File

@@ -605,7 +605,7 @@ const ChannelsTable = () => {
<Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
</>
}
style={{ width: 500 }}
style={{ width: isMobile() ? '90%' : 500 }}
bodyStyle={{ padding: '24px' }}
>
<div style={{ marginBottom: 20 }}>
@@ -633,7 +633,11 @@ const ChannelsTable = () => {
}
return (
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
<div key={column.key} style={{
width: isMobile() ? '100%' : '50%',
marginBottom: 16,
paddingRight: 8
}}>
<Checkbox
checked={!!visibleColumns[column.key]}
onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
@@ -1253,87 +1257,137 @@ const ChannelsTable = () => {
<Divider style={{ marginBottom: 15 }} />
<div
style={{
display: isMobile() ? '' : 'flex',
display: 'flex',
flexDirection: isMobile() ? 'column' : 'row',
marginTop: isMobile() ? 0 : -45,
zIndex: 999,
pointerEvents: 'none'
}}
>
<Space
style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
style={{
pointerEvents: 'auto',
marginTop: isMobile() ? 0 : 45,
marginBottom: isMobile() ? 16 : 0,
display: 'flex',
flexWrap: isMobile() ? 'wrap' : 'nowrap',
gap: '8px'
}}
>
<Typography.Text strong>{t('使用ID排序')}</Typography.Text>
<Switch
checked={idSort}
label={t('使用ID排序')}
uncheckedText={t('关')}
aria-label={t('是否用ID排序')}
onChange={(v) => {
localStorage.setItem('id-sort', v + '');
setIdSort(v);
loadChannels(0, pageSize, v, enableTagMode)
.then()
.catch((reason) => {
showError(reason);
<div style={{
display: 'flex',
alignItems: 'center',
marginRight: 16,
flexWrap: 'nowrap'
}}>
<Typography.Text strong style={{ marginRight: 8 }}>{t('使用ID排序')}</Typography.Text>
<Switch
checked={idSort}
label={t('使用ID排序')}
uncheckedText={t('关')}
aria-label={t('是否用ID排序')}
onChange={(v) => {
localStorage.setItem('id-sort', v + '');
setIdSort(v);
loadChannels(0, pageSize, v, enableTagMode)
.then()
.catch((reason) => {
showError(reason);
});
}}
></Switch>
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px'
}}>
<Button
theme="light"
type="primary"
icon={<IconPlus />}
onClick={() => {
setEditingChannel({
id: undefined
});
}}
></Switch>
<Button
theme="light"
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setEditingChannel({
id: undefined
});
setShowEdit(true);
}}
>
{t('添加渠道')}
</Button>
<Popconfirm
title={t('确定?')}
okType={'warning'}
onConfirm={testAllChannels}
position={isMobile() ? 'top' : 'top'}
>
<Button theme="light" type="warning" style={{ marginRight: 8 }}>
{t('测试所有通道')}
setShowEdit(true);
}}
>
{t('添加渠道')}
</Button>
</Popconfirm>
<Popconfirm
title={t('确定?')}
okType={'secondary'}
onConfirm={updateAllChannelsBalance}
>
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
{t('更新所有已启用通道余额')}
<Button
theme="light"
type="primary"
icon={<IconRefresh />}
onClick={refresh}
>
{t('刷新')}
</Button>
</Popconfirm>
<Popconfirm
title={t('确定是否要删除禁用通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={deleteAllDisabledChannels}
>
<Button theme="light" type="danger" style={{ marginRight: 8 }}>
{t('删除禁用通道')}
</Button>
</Popconfirm>
<Button
theme="light"
type="primary"
style={{ marginRight: 8 }}
onClick={refresh}
>
{t('刷新')}
</Button>
<Dropdown
trigger="click"
render={
<Dropdown.Menu>
<Dropdown.Item>
<Popconfirm
title={t('确定?')}
okType={'warning'}
onConfirm={testAllChannels}
position={isMobile() ? 'top' : 'top'}
>
<Button theme="light" type="warning" style={{ width: '100%' }}>
{t('测试所有通道')}
</Button>
</Popconfirm>
</Dropdown.Item>
<Dropdown.Item>
<Popconfirm
title={t('确定?')}
okType={'secondary'}
onConfirm={updateAllChannelsBalance}
>
<Button theme="light" type="secondary" style={{ width: '100%' }}>
{t('更新所有已启用通道余额')}
</Button>
</Popconfirm>
</Dropdown.Item>
<Dropdown.Item>
<Popconfirm
title={t('确定是否要删除禁用通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={deleteAllDisabledChannels}
>
<Button theme="light" type="danger" style={{ width: '100%' }}>
{t('删除禁用通道')}
</Button>
</Popconfirm>
</Dropdown.Item>
</Dropdown.Menu>
}
>
<Button theme="light" type="tertiary" icon={<IconSetting />}>
{t('批量操作')}
</Button>
</Dropdown>
</div>
</Space>
</div>
<div style={{ marginTop: 20 }}>
<Space>
<Typography.Text strong>{t('开启批量操作')}</Typography.Text>
<div style={{
marginTop: 20,
display: 'flex',
flexDirection: isMobile() ? 'column' : 'row',
alignItems: isMobile() ? 'flex-start' : 'center',
gap: isMobile() ? '8px' : '16px'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
marginBottom: isMobile() ? 8 : 0
}}>
<Typography.Text strong style={{ marginRight: 8 }}>{t('开启批量操作')}</Typography.Text>
<Switch
label={t('开启批量操作')}
uncheckedText={t('关')}
@@ -1341,20 +1395,25 @@ const ChannelsTable = () => {
onChange={(v) => {
setEnableBatchDelete(v);
}}
></Switch>
/>
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px'
}}>
<Popconfirm
title={t('确定是否要删除所选通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={batchDeleteChannels}
disabled={!enableBatchDelete}
position={'top'}
>
<Button
disabled={!enableBatchDelete}
theme="light"
type="danger"
style={{ marginRight: 8 }}
>
{t('删除所选通道')}
</Button>
@@ -1364,17 +1423,27 @@ const ChannelsTable = () => {
content={t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用')}
okType={'warning'}
onConfirm={fixChannelsAbilities}
position={'top'}
>
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
<Button theme="light" type="secondary">
{t('修复数据库一致性')}
</Button>
</Popconfirm>
</Space>
</div>
</div>
<div style={{ marginTop: 20 }}>
<Space>
<Typography.Text strong>{t('标签聚合模式')}</Typography.Text>
<div style={{
marginTop: 20,
display: 'flex',
flexDirection: isMobile() ? 'column' : 'row',
alignItems: isMobile() ? 'flex-start' : 'center',
gap: isMobile() ? '8px' : '16px'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
marginBottom: isMobile() ? 8 : 0
}}>
<Typography.Text strong style={{ marginRight: 8 }}>{t('标签聚合模式')}</Typography.Text>
<Switch
checked={enableTagMode}
label={t('标签聚合模式')}
@@ -1385,28 +1454,33 @@ const ChannelsTable = () => {
loadChannels(0, pageSize, idSort, v);
}}
/>
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px'
}}>
<Button
disabled={!enableBatchDelete}
theme="light"
type="primary"
style={{ marginRight: 8 }}
onClick={() => setShowBatchSetTag(true)}
>
{t('批量设置标签')}
</Button>
<Button
theme="light"
type="tertiary"
icon={<IconSetting />}
onClick={() => setShowColumnSelector(true)}
style={{ marginRight: 8 }}
>
{t('列设置')}
</Button>
</Space>
</div>
</div>
<Table
loading={loading}
columns={getVisibleColumns()}
@@ -1423,6 +1497,7 @@ const ChannelsTable = () => {
},
onPageChange: handlePageChange
}}
expandAllRows={false}
onRow={handleRow}
rowSelection={
enableBatchDelete
@@ -1442,6 +1517,7 @@ const ChannelsTable = () => {
onCancel={() => setShowBatchSetTag(false)}
maskClosable={false}
centered={true}
style={{ width: isMobile() ? '90%' : 500 }}
>
<div style={{ marginBottom: 20 }}>
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
@@ -1450,7 +1526,13 @@ const ChannelsTable = () => {
placeholder={t('请输入标签名称')}
value={batchSetTagValue}
onChange={(v) => setBatchSetTagValue(v)}
size="large"
/>
<div style={{ marginTop: 16 }}>
<Typography.Text type="secondary">
{t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
</Typography.Text>
</div>
</Modal>
{/* 模型测试弹窗 */}
@@ -1464,7 +1546,6 @@ const ChannelsTable = () => {
footer={null}
maskClosable={true}
centered={true}
width={600}
>
<div style={{ maxHeight: '500px', overflowY: 'auto', padding: '10px' }}>
{currentTestChannel && (
@@ -1477,8 +1558,9 @@ const ChannelsTable = () => {
<Input
placeholder={t('搜索模型...')}
value={modelSearchKeyword}
onChange={(value) => setModelSearchKeyword(value)}
onChange={(v) => setModelSearchKeyword(v)}
style={{ marginBottom: '16px' }}
prefix={<IconFilter />}
showClear
/>

View File

@@ -1,12 +1,14 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui';
import { StyleContext } from '../context/Style/index.js';
const FooterBar = () => {
const { t } = useTranslation();
const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML());
const [styleState] = useContext(StyleContext);
let remainCheckTimes = 5;
const loadFooter = () => {
@@ -57,7 +59,10 @@ const FooterBar = () => {
}, []);
return (
<div style={{ textAlign: 'center' }}>
<div style={{
textAlign: 'center',
paddingBottom: '5px',
}}>
{footer ? (
<div
className='custom-footer'

View File

@@ -6,6 +6,7 @@ import { API, showError, showSuccess } from '../helpers';
import { useTranslation } from 'react-i18next';
import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js';
import SettingClaudeModel from '../pages/Setting/Model/SettingClaudeModel.js';
import SettingGlobalModel from '../pages/Setting/Model/SettingGlobalModel.js';
const ModelSetting = () => {
const { t } = useTranslation();
@@ -16,6 +17,7 @@ const ModelSetting = () => {
'claude.thinking_adapter_enabled': true,
'claude.default_max_tokens': '',
'claude.thinking_adapter_budget_tokens_percentage': 0.8,
'global.pass_through_request_enabled': false,
});
let [loading, setLoading] = useState(false);
@@ -35,7 +37,7 @@ const ModelSetting = () => {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
if (
item.key.endsWith('Enabled')
item.key.endsWith('Enabled') || item.key.endsWith('enabled')
) {
newInputs[item.key] = item.value === 'true' ? true : false;
} else {
@@ -67,6 +69,10 @@ const ModelSetting = () => {
return (
<>
<Spin spinning={loading} size='large'>
{/* OpenAI */}
<Card style={{ marginTop: '10px' }}>
<SettingGlobalModel options={inputs} refresh={onRefresh} />
</Card>
{/* Gemini */}
<Card style={{ marginTop: '10px' }}>
<SettingGeminiModel options={inputs} refresh={onRefresh} />

View File

@@ -7,7 +7,6 @@ import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';

View File

@@ -71,12 +71,17 @@ const PageLayout = () => {
const isSidebarCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true';
return (
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Layout style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: styleState.isMobile ? 'visible' : 'hidden'
}}>
<Header style={{
padding: 0,
height: 'auto',
lineHeight: 'normal',
position: 'fixed',
position: styleState.isMobile ? 'sticky' : 'fixed',
width: '100%',
top: 0,
zIndex: 100,
@@ -84,39 +89,51 @@ const PageLayout = () => {
}}>
<HeaderBar />
</Header>
<Layout style={{ marginTop: '56px', height: 'calc(100vh - 56px)', overflow: 'hidden' }}>
<Layout style={{
marginTop: styleState.isMobile ? '0' : '56px',
height: styleState.isMobile ? 'auto' : 'calc(100vh - 56px)',
overflow: styleState.isMobile ? 'visible' : 'auto',
display: 'flex',
flexDirection: 'column'
}}>
{styleState.showSider && (
<Sider style={{
height: 'calc(100vh - 56px)',
<Sider style={{
position: 'fixed',
left: 0,
top: '56px',
zIndex: 90,
overflowY: 'auto',
overflowX: 'hidden',
width: 'auto',
background: 'transparent',
boxShadow: 'none',
zIndex: 99,
background: 'var(--semi-color-bg-1)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
border: 'none',
paddingRight: '5px'
paddingRight: '0',
height: 'calc(100vh - 56px)',
}}>
<SiderBar />
</Sider>
)}
<Layout style={{
marginLeft: styleState.showSider ? (isSidebarCollapsed ? '60px' : '200px') : '0',
transition: 'margin-left 0.3s ease'
marginLeft: styleState.isMobile ? '0' : (styleState.showSider ? (styleState.siderCollapsed ? '60px' : '200px') : '0'),
transition: 'margin-left 0.3s ease',
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column'
}}>
<Content
style={{
height: '100%',
overflowY: 'auto',
padding: styleState.shouldInnerPadding? '24px': '0'
flex: '1 0 auto',
overflowY: styleState.isMobile ? 'visible' : 'auto',
WebkitOverflowScrolling: 'touch',
padding: styleState.shouldInnerPadding? '24px': '0',
position: 'relative',
marginTop: styleState.isMobile ? '2px' : '0',
}}
>
<App />
</Content>
<Layout.Footer>
<Layout.Footer style={{
flex: '0 0 auto',
width: '100%'
}}>
<FooterBar />
</Layout.Footer>
</Layout>

View File

@@ -1,16 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
import { API, showError, showSuccess } from '../helpers';

View File

@@ -33,12 +33,12 @@ import { setStatusData } from '../helpers/data.js';
import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js';
import { StyleContext } from '../context/Style/index.js';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
// 自定义侧边栏按钮样式
const navItemStyle = {
borderRadius: '6px',
margin: '4px 8px',
transition: 'all 0.3s ease'
};
// 自定义侧边栏按钮悬停样式
@@ -59,10 +59,28 @@ const iconStyle = (itemKey, selectedKeys) => {
return {
fontSize: '18px',
color: selectedKeys.includes(itemKey) ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)',
transition: 'all 0.3s ease'
};
};
// Define routerMap as a constant outside the component
const routerMap = {
home: '/',
channel: '/channel',
token: '/token',
redemption: '/redemption',
topup: '/topup',
user: '/user',
log: '/log',
midjourney: '/midjourney',
setting: '/setting',
about: '/about',
detail: '/detail',
pricing: '/pricing',
task: '/task',
playground: '/playground',
personal: '/personal',
};
const SiderBar = () => {
const { t } = useTranslation();
const [styleState, styleDispatch] = useContext(StyleContext);
@@ -77,10 +95,11 @@ const SiderBar = () => {
const theme = useTheme();
const setTheme = useSetTheme();
const location = useLocation();
const [routerMapState, setRouterMapState] = useState(routerMap);
// 预先计算所有可能的图标样式
const allItemKeys = useMemo(() => {
const keys = ['home', 'channel', 'token', 'redemption', 'topup', 'user', 'log', 'midjourney',
const keys = ['home', 'channel', 'token', 'redemption', 'topup', 'user', 'log', 'midjourney',
'setting', 'about', 'chat', 'detail', 'pricing', 'task', 'playground', 'personal'];
// 添加聊天项的keys
for (let i = 0; i < chatItems.length; i++) {
@@ -98,25 +117,6 @@ const SiderBar = () => {
return styles;
}, [allItemKeys, selectedKeys]);
const routerMap = {
home: '/',
channel: '/channel',
token: '/token',
redemption: '/redemption',
topup: '/topup',
user: '/user',
log: '/log',
midjourney: '/midjourney',
setting: '/setting',
about: '/about',
chat: '/chat',
detail: '/detail',
pricing: '/pricing',
task: '/task',
playground: '/playground',
personal: '/personal',
};
const workspaceItems = useMemo(
() => [
{
@@ -238,19 +238,24 @@ const SiderBar = () => {
[chatItems, t],
);
useEffect(() => {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
// Function to update router map with chat routes
const updateRouterMapWithChats = (chats) => {
const newRouterMap = { ...routerMap };
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/chat/')) {
setSelectedKeys(['chat']);
if (Array.isArray(chats) && chats.length > 0) {
for (let i = 0; i < chats.length; i++) {
newRouterMap['chat' + i] = '/chat/' + i;
}
}
setRouterMapState(newRouterMap);
return newRouterMap;
};
// Update the useEffect for chat items
useEffect(() => {
let chats = localStorage.getItem('chats');
if (chats) {
// console.log(chats);
try {
chats = JSON.parse(chats);
if (Array.isArray(chats)) {
@@ -262,19 +267,44 @@ const SiderBar = () => {
chat.itemKey = 'chat' + i;
chat.to = '/chat/' + i;
}
// setRouterMap({ ...routerMap, chat: '/chat/' + i })
chatItems.push(chat);
}
setChatItems(chatItems);
// Update router map with chat routes
updateRouterMapWithChats(chats);
}
} catch (e) {
console.error(e);
showError('聊天数据解析失败')
}
}
}, []);
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
}, [location.pathname]);
// Update the useEffect for route selection
useEffect(() => {
const currentPath = location.pathname;
let matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath);
// Handle chat routes
if (!matchingKey && currentPath.startsWith('/chat/')) {
const chatIndex = currentPath.split('/').pop();
if (!isNaN(chatIndex)) {
matchingKey = 'chat' + chatIndex;
} else {
matchingKey = 'chat';
}
}
// If we found a matching key, update the selected keys
if (matchingKey) {
setSelectedKeys([matchingKey]);
}
}, [location.pathname, routerMapState]);
useEffect(() => {
setIsCollapsed(styleState.siderCollapsed);
}, [styleState.siderCollapsed]);
// Custom divider style
const dividerStyle = {
@@ -298,14 +328,15 @@ const SiderBar = () => {
className="custom-sidebar-nav"
style={{
width: isCollapsed ? '60px' : '200px',
height: '100%',
boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
borderRight: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-bg-0)',
borderRadius: '0 8px 8px 0',
transition: 'all 0.3s ease',
background: 'var(--semi-color-bg-1)',
borderRadius: styleState.isMobile ? '0' : '0 8px 8px 0',
position: 'relative',
zIndex: 95
zIndex: 95,
height: '100%',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
}}
defaultIsCollapsed={
localStorage.getItem('default_collapse_sidebar') === 'true'
@@ -313,21 +344,21 @@ const SiderBar = () => {
isCollapsed={isCollapsed}
onCollapseChange={(collapsed) => {
setIsCollapsed(collapsed);
// styleDispatch({ type: 'SET_SIDER', payload: true });
styleDispatch({ type: 'SET_SIDER_COLLAPSED', payload: collapsed });
localStorage.setItem('default_collapse_sidebar', collapsed);
// 始终保持侧边栏显示,只是宽度不同
styleDispatch({ type: 'SET_SIDER', payload: true });
// 确保在收起侧边栏时有选中的项目,避免不必要的计算
if (selectedKeys.length === 0) {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
const matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/chat/')) {
setSelectedKeys(['chat']);
} else {
setSelectedKeys(['home']); // 默认选中首页
setSelectedKeys(['detail']); // 默认选中首页
}
}
}}
@@ -336,28 +367,10 @@ const SiderBar = () => {
hoverStyle={navItemHoverStyle}
selectedStyle={navItemSelectedStyle}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
let chats = localStorage.getItem('chats');
if (chats) {
chats = JSON.parse(chats);
if (Array.isArray(chats) && chats.length > 0) {
for (let i = 0; i < chats.length; i++) {
routerMap['chat' + i] = '/chat/' + i;
}
if (chats.length > 1) {
// delete /chat
if (routerMap['chat']) {
delete routerMap['chat'];
}
} else {
// rename /chat to /chat/0
routerMap['chat'] = '/chat/0';
}
}
}
return (
<Link
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
to={routerMapState[props.itemKey] || routerMap[props.itemKey]}
>
{itemElement}
</Link>
@@ -417,7 +430,7 @@ const SiderBar = () => {
<Divider style={dividerStyle} />
{/* Workspace Section */}
{!isCollapsed && <div style={groupLabelStyle}>{t('控制台')}</div>}
{!isCollapsed && <Text style={groupLabelStyle}>{t('控制台')}</Text>}
{workspaceItems.map((item) => (
<Nav.Item
key={item.itemKey}
@@ -434,7 +447,7 @@ const SiderBar = () => {
<Divider style={dividerStyle} />
{/* Admin Section */}
{!isCollapsed && <div style={groupLabelStyle}>{t('管理员')}</div>}
{!isCollapsed && <Text style={groupLabelStyle}>{t('管理员')}</Text>}
{adminItems.map((item) => (
<Nav.Item
key={item.itemKey}
@@ -451,7 +464,7 @@ const SiderBar = () => {
<Divider style={dividerStyle} />
{/* Finance Management Section */}
{!isCollapsed && <div style={groupLabelStyle}>{t('个人中心')}</div>}
{!isCollapsed && <Text style={groupLabelStyle}>{t('个人中心')}</Text>}
{financeItems.map((item) => (
<Nav.Item
key={item.itemKey}
@@ -463,12 +476,10 @@ const SiderBar = () => {
))}
<Nav.Footer
collapseButton={true}
style={{
borderTop: '1px solid var(--semi-color-border)',
padding: '12px 0',
marginTop: 'auto'
paddingBottom: styleState?.isMobile ? '112px' : '',
}}
collapseButton={true}
collapseText={(collapsed)=>
{
if(collapsed){

View File

@@ -9,8 +9,9 @@ export const StyleContext = React.createContext({
export const StyleProvider = ({ children }) => {
const [state, setState] = useState({
isMobile: false,
isMobile: isMobile(),
showSider: false,
siderCollapsed: false,
shouldInnerPadding: false,
});
@@ -26,6 +27,9 @@ export const StyleProvider = ({ children }) => {
case 'SET_MOBILE':
setState(prev => ({ ...prev, isMobile: action.payload }));
break;
case 'SET_SIDER_COLLAPSED':
setState(prev => ({ ...prev, siderCollapsed: action.payload }));
break
case 'SET_INNER_PADDING':
setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
break;
@@ -39,7 +43,13 @@ export const StyleProvider = ({ children }) => {
useEffect(() => {
const updateIsMobile = () => {
dispatch({ type: 'SET_MOBILE', payload: isMobile() });
const mobileDetected = isMobile();
dispatch({ type: 'SET_MOBILE', payload: mobileDetected });
// If on mobile, we might want to auto-hide the sidebar
if (mobileDetected && state.showSider) {
dispatch({ type: 'SET_SIDER', payload: false });
}
};
updateIsMobile();
@@ -51,24 +61,31 @@ export const StyleProvider = ({ children }) => {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
dispatch({ type: 'SET_SIDER', payload: true });
// Only show sidebar on non-mobile devices by default
dispatch({ type: 'SET_SIDER', payload: !isMobile() });
dispatch({ type: 'SET_INNER_PADDING', payload: true });
}
if (isMobile()) {
dispatch({ type: 'SET_SIDER', payload: false });
}
};
updateShowSider()
updateShowSider();
const updateSiderCollapsed = () => {
const isCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true';
dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
};
// Optionally, add event listeners to handle window resize
window.addEventListener('resize', updateIsMobile);
updateSiderCollapsed();
// Add event listeners to handle window resize
const handleResize = () => {
updateIsMobile();
};
window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', updateIsMobile);
window.removeEventListener('resize', handleResize);
};
}, []);

View File

@@ -1,5 +1,6 @@
{
"主页": "Home",
"文档": "Docs",
"控制台": "Console",
"$%.6f 额度": "$%.6f quota",
"%d 点额度": "%d point quota",
@@ -192,6 +193,8 @@
"通用设置": "General Settings",
"充值链接": "Recharge Link",
"例如发卡网站的购买链接": "E.g., purchase link from card issuing website",
"文档地址": "Document Link",
"例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro",
"聊天页面链接": "Chat Page Link",
"例如 ChatGPT Next Web 的部署地址": "E.g., ChatGPT Next Web deployment address",
"单位美元额度": "Quota per USD",

View File

@@ -1,7 +1,6 @@
body {
margin: 0;
padding-top: 0;
overflow: hidden;
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
sans-serif;
-webkit-font-smoothing: antialiased;
@@ -13,7 +12,8 @@ body {
}
#root {
height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
@@ -82,6 +82,31 @@ body {
.semi-navigation-horizontal .semi-navigation-header {
margin-right: 0;
}
/* 确保移动端内容可滚动 */
.semi-layout-content {
-webkit-overflow-scrolling: touch !important;
overscroll-behavior-y: auto !important;
}
/* 修复移动端下拉刷新 */
body {
overflow: visible !important;
overscroll-behavior-y: auto !important;
position: static !important;
height: 100% !important;
}
/* 确保内容区域在移动端可以正常滚动 */
#root {
overflow: visible !important;
height: 100% !important;
}
/* 隐藏在移动设备上 */
.hide-on-mobile {
display: none !important;
}
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
@@ -162,14 +187,14 @@ code {
}
}
.semi-navigation-vertical {
/*flex: 0 0 auto;*/
/*display: flex;*/
/*flex-direction: column;*/
/*width: 100%;*/
height: 100%;
overflow: hidden;
}
/*.semi-navigation-vertical {*/
/* !*flex: 0 0 auto;*!*/
/* !*display: flex;*!*/
/* !*flex-direction: column;*!*/
/* !*width: 100%;*!*/
/* height: 100%;*/
/* overflow: hidden;*/
/*}*/
.main-content {
padding: 4px;
@@ -184,12 +209,6 @@ code {
font-size: 1.1em;
}
@media only screen and (max-width: 600px) {
.hide-on-mobile {
display: none !important;
}
}
/* 顶部栏样式 */
.topnav {
padding: 0 16px;
@@ -248,8 +267,9 @@ code {
}
/* Custom sidebar shadow */
.custom-sidebar-nav {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
}
/*.custom-sidebar-nav {*/
/* box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
/* -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
/* -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
/* min-height: 100%;*/
/*}*/

View File

@@ -88,7 +88,7 @@ export default function SettingClaudeModel(props) {
>
<Form.Section text={t('Claude设置')}>
<Row>
<Col span={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea
label={t('Claude请求头覆盖')}
field={'claude.model_headers_settings'}
@@ -108,7 +108,7 @@ export default function SettingClaudeModel(props) {
</Col>
</Row>
<Row>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea
label={t('缺省 MaxTokens')}
field={'claude.default_max_tokens'}
@@ -145,7 +145,7 @@ export default function SettingClaudeModel(props) {
</Col>
</Row>
<Row>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('思考适配 BudgetTokens 百分比')}
field={'claude.thinking_adapter_budget_tokens_percentage'}

View File

@@ -86,7 +86,7 @@ export default function SettingGeminiModel(props) {
>
<Form.Section text={t('Gemini设置')}>
<Row>
<Col span={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea
label={t('Gemini安全设置')}
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)}
@@ -106,7 +106,7 @@ export default function SettingGeminiModel(props) {
</Col>
</Row>
<Row>
<Col span={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea
label={t('Gemini版本设置')}
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)}

View File

@@ -0,0 +1,98 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning, verifyJSON
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingGlobalModel(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
'global.pass_through_request_enabled': false,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={t('全局设置')}>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
label={t('启用请求透传')}
field={'global.pass_through_request_enabled'}
onChange={(value) => setInputs({ ...inputs, 'global.pass_through_request_enabled': value })}
extraText={'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'}
/>
</Col>
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
{t('保存')}
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}

View File

@@ -86,7 +86,7 @@ export default function GroupRatioSettings(props) {
>
<Form.Section text={t('分组设置')}>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('分组倍率')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
@@ -105,7 +105,7 @@ export default function GroupRatioSettings(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('用户可选分组')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}

View File

@@ -102,7 +102,7 @@ export default function ModelRatioSettings(props) {
>
<Form.Section>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('模型固定价格')}
extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
@@ -122,7 +122,7 @@ export default function ModelRatioSettings(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('模型倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
@@ -141,7 +141,7 @@ export default function ModelRatioSettings(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('提示缓存倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
@@ -160,7 +160,7 @@ export default function ModelRatioSettings(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('模型补全倍率(仅对自定义模型有效)')}
extraText={t('仅对自定义模型有效')}

View File

@@ -76,7 +76,7 @@ export default function SettingsCreditLimit(props) {
>
<Form.Section text={t('额度设置')}>
<Row gutter={16}>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('新用户初始额度')}
field={'QuotaForNewUser'}
@@ -92,7 +92,7 @@ export default function SettingsCreditLimit(props) {
}
/>
</Col>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('请求预扣费额度')}
field={'PreConsumedQuota'}
@@ -109,7 +109,7 @@ export default function SettingsCreditLimit(props) {
}
/>
</Col>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('邀请新用户奖励额度')}
field={'QuotaForInviter'}
@@ -126,7 +126,9 @@ export default function SettingsCreditLimit(props) {
}
/>
</Col>
<Col span={6}>
</Row>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={6}>
<Form.InputNumber
label={t('新用户使用邀请码奖励额度')}
field={'QuotaForInvitee'}

View File

@@ -86,7 +86,7 @@ export default function DataDashboard(props) {
>
<Form.Section text={t('数据看板设置')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DataExportEnabled'}
label={t('启用数据看板(实验性)')}
@@ -103,7 +103,7 @@ export default function DataDashboard(props) {
</Col>
</Row>
<Row>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('数据看板更新间隔')}
step={1}
@@ -120,7 +120,7 @@ export default function DataDashboard(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Select
label={t('数据看板默认时间粒度')}
optionList={optionsDataExportDefaultTime}

View File

@@ -80,7 +80,7 @@ export default function SettingsDrawing(props) {
>
<Form.Section text={t('绘图设置')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DrawingEnabled'}
label={t('启用绘图功能')}
@@ -95,7 +95,7 @@ export default function SettingsDrawing(props) {
}}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'MjNotifyEnabled'}
label={t('允许回调(会泄露服务器 IP 地址)')}
@@ -110,7 +110,7 @@ export default function SettingsDrawing(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'MjAccountFilterEnabled'}
label={t('允许 AccountFilter 参数')}
@@ -125,7 +125,7 @@ export default function SettingsDrawing(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'MjForwardUrlEnabled'}
label={t('开启之后将上游地址替换为服务器地址')}
@@ -140,7 +140,7 @@ export default function SettingsDrawing(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'MjModeClearEnabled'}
label={
@@ -160,7 +160,7 @@ export default function SettingsDrawing(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'MjActionCheckSuccessEnabled'}
label={t('检测必须等待绘图成功才能进行放大等操作')}

View File

@@ -92,7 +92,7 @@ export default function GeneralSettings(props) {
>
<Form.Section text={t('通用设置')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'TopUpLink'}
label={t('充值链接')}
@@ -102,7 +102,7 @@ export default function GeneralSettings(props) {
showClear
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'general_setting.docs_link'}
label={t('文档地址')}
@@ -112,7 +112,7 @@ export default function GeneralSettings(props) {
showClear
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'QuotaPerUnit'}
label={t('单位美元额度')}
@@ -123,7 +123,7 @@ export default function GeneralSettings(props) {
onClick={() => setShowQuotaWarning(true)}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'RetryTimes'}
label={t('失败重试次数')}
@@ -135,7 +135,7 @@ export default function GeneralSettings(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DisplayInCurrencyEnabled'}
label={t('以货币形式显示额度')}
@@ -150,7 +150,7 @@ export default function GeneralSettings(props) {
}}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DisplayTokenStatEnabled'}
label={t('额度查询接口返回令牌额度而非用户额度')}
@@ -165,7 +165,7 @@ export default function GeneralSettings(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DefaultCollapseSidebar'}
label={t('默认折叠侧边栏')}
@@ -182,7 +182,7 @@ export default function GeneralSettings(props) {
</Col>
</Row>
<Row>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DemoSiteEnabled'}
label={t('演示站点模式')}
@@ -197,7 +197,7 @@ export default function GeneralSettings(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'SelfUseModeEnabled'}
label={t('自用模式')}

View File

@@ -100,7 +100,7 @@ export default function SettingsLog(props) {
>
<Form.Section text={t('日志设置')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'LogConsumeEnabled'}
label={t('启用额度消费日志记录')}
@@ -115,7 +115,7 @@ export default function SettingsLog(props) {
}}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Spin spinning={loadingCleanHistoryLog}>
<Form.DatePicker
label={t('日志记录时间')}

View File

@@ -1,273 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
verifyJSONPromise
} from '../../../helpers';
export default function SettingsMagnification(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ModelPrice: '',
ModelRatio: '',
CompletionRatio: '',
GroupRatio: '',
UserUsableGroups: ''
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
async function onSubmit() {
try {
console.log('Starting validation...');
await refForm.current.validate().then(() => {
console.log('Validation passed');
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = inputs[item.key];
}
return API.put('/api/option/', {
key: item.key,
value
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError('部分保存失败,请重试');
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message)
}
}
showSuccess('保存成功');
props.refresh();
})
.catch(error => {
console.error('Unexpected error in Promise.all:', error);
showError('保存失败,请重试');
})
.finally(() => {
setLoading(false);
});
}).catch((error) => {
console.error('Validation failed:', error);
showError('请检查输入');
});
} catch (error) {
showError('请检查输入');
console.error(error);
}
}
async function resetModelRatio() {
try {
let res = await API.post(`/api/option/rest_model_ratio`);
// return {success, message}
if (res.data.success) {
showSuccess(res.data.message);
props.refresh();
} else {
showError(res.data.message);
}
} catch (error) {
showError(error);
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'倍率设置'}>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'模型固定价格'}
extraText={'一次调用消耗多少刀,优先级大于模型倍率'}
placeholder={
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀'
}
field={'ModelPrice'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) =>
setInputs({
...inputs,
ModelPrice: value
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'模型倍率'}
extraText={''}
placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
field={'ModelRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) =>
setInputs({
...inputs,
ModelRatio: value
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'模型补全倍率(仅对自定义模型有效)'}
extraText={'仅对自定义模型有效'}
placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
field={'CompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) =>
setInputs({
...inputs,
CompletionRatio: value
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'分组倍率'}
extraText={''}
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
field={'GroupRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) =>
setInputs({
...inputs,
GroupRatio: value
})
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'用户可选分组'}
extraText={''}
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
field={'UserUsableGroups'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) =>
setInputs({
...inputs,
UserUsableGroups: value
})
}
/>
</Col>
</Row>
</Form.Section>
</Form>
<Space>
<Button onClick={onSubmit}>
保存倍率设置
</Button>
<Popconfirm
title='确定重置模型倍率吗?'
content='此修改将不可逆'
okType={'danger'}
position={'top'}
onConfirm={() => {
resetModelRatio();
}}
>
<Button type={'danger'}>
重置模型倍率
</Button>
</Popconfirm>
</Space>
</Spin>
);
}

View File

@@ -78,7 +78,7 @@ export default function SettingsMonitoring(props) {
>
<Form.Section text={t('监控设置')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('测试所有渠道的最长响应时间')}
step={1}
@@ -95,7 +95,7 @@ export default function SettingsMonitoring(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('额度提醒阈值')}
step={1}
@@ -114,7 +114,7 @@ export default function SettingsMonitoring(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'AutomaticDisableChannelEnabled'}
label={t('失败时自动禁用通道')}
@@ -129,7 +129,7 @@ export default function SettingsMonitoring(props) {
}}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'AutomaticEnableChannelEnabled'}
label={t('成功时自动启用通道')}
@@ -146,7 +146,7 @@ export default function SettingsMonitoring(props) {
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('自动禁用关键词')}
placeholder={t('一行一个,不区分大小写')}

View File

@@ -75,7 +75,7 @@ export default function SettingsSensitiveWords(props) {
>
<Form.Section text={t('屏蔽词过滤设置')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'CheckSensitiveEnabled'}
label={t('启用屏蔽词过滤功能')}
@@ -90,7 +90,7 @@ export default function SettingsSensitiveWords(props) {
}}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'CheckSensitiveOnPromptEnabled'}
label={t('启用 Prompt 检查')}
@@ -107,7 +107,7 @@ export default function SettingsSensitiveWords(props) {
</Col>
</Row>
<Row>
<Col span={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.TextArea
label={t('屏蔽词列表')}
extraText={t('一行一个屏蔽词,不需要符号分割')}

View File

@@ -78,7 +78,7 @@ export default function RequestRateLimit(props) {
>
<Form.Section text={t('模型请求速率限制')}>
<Row gutter={16}>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'ModelRequestRateLimitEnabled'}
label={t('启用用户模型请求速率限制(可能会影响高并发性能)')}
@@ -95,7 +95,7 @@ export default function RequestRateLimit(props) {
</Col>
</Row>
<Row>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('限制周期')}
step={1}
@@ -113,7 +113,7 @@ export default function RequestRateLimit(props) {
</Col>
</Row>
<Row>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('用户每周期最多请求次数')}
step={1}
@@ -129,7 +129,7 @@ export default function RequestRateLimit(props) {
}
/>
</Col>
<Col span={8}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('用户每周期最多请求完成次数')}
step={1}

View File

@@ -52,6 +52,7 @@ export default defineConfig({
},
},
server: {
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3000',