mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-07 07:39:42 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb5b5630c | ||
|
|
e1b9f164f9 | ||
|
|
69db1f1465 | ||
|
|
94549f9687 | ||
|
|
c7e1bab18a | ||
|
|
627f95b034 | ||
|
|
8b99eec440 | ||
|
|
49bfd2b719 | ||
|
|
434e9d7695 | ||
|
|
b2938ffe2c |
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
|
||||
@@ -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: styleState?.isMobile ? '112px' : '5px',
|
||||
}}>
|
||||
{footer ? (
|
||||
<div
|
||||
className='custom-footer'
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -71,7 +71,12 @@ 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: 'hidden'
|
||||
}}>
|
||||
<Header style={{
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
@@ -84,39 +89,50 @@ const PageLayout = () => {
|
||||
}}>
|
||||
<HeaderBar />
|
||||
</Header>
|
||||
<Layout style={{ marginTop: '56px', height: 'calc(100vh - 56px)', overflow: 'hidden' }}>
|
||||
<Layout style={{
|
||||
marginTop: '56px',
|
||||
height: 'calc(100vh - 56px)',
|
||||
overflow: '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: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
padding: styleState.shouldInnerPadding? '24px': '0',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
<Layout.Footer>
|
||||
<Layout.Footer style={{
|
||||
flex: '0 0 auto',
|
||||
width: '100%'
|
||||
}}>
|
||||
<FooterBar />
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,7 +59,6 @@ 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'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,7 +79,7 @@ const SiderBar = () => {
|
||||
|
||||
// 预先计算所有可能的图标样式
|
||||
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++) {
|
||||
@@ -241,13 +240,15 @@ const SiderBar = () => {
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
|
||||
|
||||
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
} else if (currentPath.startsWith('/chat/')) {
|
||||
setSelectedKeys(['chat']);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
// console.log(chats);
|
||||
@@ -273,8 +274,8 @@ const SiderBar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
|
||||
}, [location.pathname]);
|
||||
setIsCollapsed(styleState.siderCollapsed);
|
||||
})
|
||||
|
||||
// Custom divider style
|
||||
const dividerStyle = {
|
||||
@@ -298,14 +299,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 +315,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);
|
||||
|
||||
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
} else if (currentPath.startsWith('/chat/')) {
|
||||
setSelectedKeys(['chat']);
|
||||
} else {
|
||||
setSelectedKeys(['home']); // 默认选中首页
|
||||
setSelectedKeys([]); // 默认选中首页
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -417,7 +419,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 +436,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 +453,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 +465,10 @@ const SiderBar = () => {
|
||||
))}
|
||||
|
||||
<Nav.Footer
|
||||
collapseButton={true}
|
||||
style={{
|
||||
borderTop: '1px solid var(--semi-color-border)',
|
||||
padding: '12px 0',
|
||||
marginTop: 'auto'
|
||||
paddingBottom: styleState?.isMobile ? '112px' : '20px',
|
||||
}}
|
||||
collapseButton={true}
|
||||
collapseText={(collapsed)=>
|
||||
{
|
||||
if(collapsed){
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -82,6 +82,16 @@ body {
|
||||
.semi-navigation-horizontal .semi-navigation-header {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* 确保移动端内容可滚动 */
|
||||
.semi-layout-content {
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
/* 隐藏在移动设备上 */
|
||||
.hide-on-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
@@ -162,14 +172,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 +194,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 +252,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%;*/
|
||||
/*}*/
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 文本,键为分组名称,值为分组描述')}
|
||||
|
||||
@@ -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('仅对自定义模型有效')}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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('检测必须等待绘图成功才能进行放大等操作')}
|
||||
|
||||
@@ -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('自用模式')}
|
||||
|
||||
@@ -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('日志记录时间')}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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('一行一个,不区分大小写')}
|
||||
|
||||
@@ -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('一行一个屏蔽词,不需要符号分割')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -52,6 +52,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
|
||||
Reference in New Issue
Block a user