Compare commits

...

13 Commits

Author SHA1 Message Date
CaIon c2a37d83a7 feat: Add new localization strings for system initialization 2025-04-03 19:27:25 +08:00
CaIon a1f6781c0e fix: Update option key from SelfUseModeEnabled to DemoSiteEnabled in PostSetup function 2025-04-03 19:21:53 +08:00
CaIon 88f7c0670f feat: Add timestamp and version to setup initialization in PostSetup function 2025-04-03 19:16:17 +08:00
CaIon c08edc315d fix: Correct option key for SelfUseModeEnabled in setup controller 2025-04-03 19:15:04 +08:00
CaIon 683accf05b Merge remote-tracking branch 'origin/main' 2025-04-03 19:09:26 +08:00
CaIon d8c10dcb51 Update README.md 2025-04-03 19:09:13 +08:00
Calcium-Ion 3e55cbcda2 Merge pull request #925 from Calcium-Ion/setup
 feat: Implement system setup functionality
2025-04-03 19:01:45 +08:00
CaIon 07d31760da feat: Refine personal mode description in setup page for clarity 2025-04-03 19:01:16 +08:00
CaIon f33ebc8e2c feat: Implement system setup functionality 2025-04-03 18:57:15 +08:00
CaIon 1de0d9123b Merge remote-tracking branch 'origin/main' 2025-04-03 17:33:03 +08:00
CaIon f0925dc105 feat: Enhance user settings and notification options 2025-04-03 17:32:48 +08:00
Calcium-Ion d9223147f2 Merge pull request #909 from jasinliu/feature/fix-dify-thinking
feat: fix dify thinking
2025-04-03 16:23:12 +08:00
jasinliu 111cedf795 fix dify thinking 2025-03-28 00:21:27 +08:00
20 changed files with 4503 additions and 3396 deletions
-1
View File
@@ -117,7 +117,6 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
> [!TIP]
> 最新版Docker镜像:`calciumion/new-api:latest`
> 默认账号root 密码123456
### 多机部署注意事项
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致
+3
View File
@@ -0,0 +1,3 @@
package constant
var Setup = false
+6 -5
View File
@@ -1,11 +1,12 @@
package constant
var (
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型
)
var (
+6 -4
View File
@@ -105,6 +105,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
request := buildTestRequest(testModel)
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
if err != nil {
return err, nil
}
adaptor.Init(info)
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
@@ -143,10 +148,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
return err, nil
}
info.PromptTokens = usage.PromptTokens
priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens))
if err != nil {
return err, nil
}
quota := 0
if !priceData.UsePrice {
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
+2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/setting"
"one-api/setting/operation_setting"
@@ -72,6 +73,7 @@ func GetStatus(c *gin.Context) {
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
"setup": constant.Setup,
},
})
return
+173
View File
@@ -0,0 +1,173 @@
package controller
import (
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/setting/operation_setting"
"time"
)
type Setup struct {
Status bool `json:"status"`
RootInit bool `json:"root_init"`
DatabaseType string `json:"database_type"`
}
type SetupRequest struct {
Username string `json:"username"`
Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"`
SelfUseModeEnabled bool `json:"SelfUseModeEnabled"`
DemoSiteEnabled bool `json:"DemoSiteEnabled"`
}
func GetSetup(c *gin.Context) {
setup := Setup{
Status: constant.Setup,
}
if constant.Setup {
c.JSON(200, gin.H{
"success": true,
"data": setup,
})
return
}
setup.RootInit = model.RootUserExists()
if common.UsingMySQL {
setup.DatabaseType = "mysql"
}
if common.UsingPostgreSQL {
setup.DatabaseType = "postgres"
}
if common.UsingSQLite {
setup.DatabaseType = "sqlite"
}
c.JSON(200, gin.H{
"success": true,
"data": setup,
})
}
func PostSetup(c *gin.Context) {
// Check if setup is already completed
if constant.Setup {
c.JSON(400, gin.H{
"success": false,
"message": "系统已经初始化完成",
})
return
}
// Check if root user already exists
rootExists := model.RootUserExists()
var req SetupRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(400, gin.H{
"success": false,
"message": "请求参数有误",
})
return
}
// If root doesn't exist, validate and create admin account
if !rootExists {
// Validate password
if req.Password != req.ConfirmPassword {
c.JSON(400, gin.H{
"success": false,
"message": "两次输入的密码不一致",
})
return
}
if len(req.Password) < 8 {
c.JSON(400, gin.H{
"success": false,
"message": "密码长度至少为8个字符",
})
return
}
// Create root user
hashedPassword, err := common.Password2Hash(req.Password)
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "系统错误: " + err.Error(),
})
return
}
rootUser := model.User{
Username: req.Username,
Password: hashedPassword,
Role: common.RoleRootUser,
Status: common.UserStatusEnabled,
DisplayName: "Root User",
AccessToken: nil,
Quota: 100000000,
}
err = model.DB.Create(&rootUser).Error
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "创建管理员账号失败: " + err.Error(),
})
return
}
}
// Set operation modes
operation_setting.SelfUseModeEnabled = req.SelfUseModeEnabled
operation_setting.DemoSiteEnabled = req.DemoSiteEnabled
// Save operation modes to database for persistence
err = model.UpdateOption("SelfUseModeEnabled", boolToString(req.SelfUseModeEnabled))
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "保存自用模式设置失败: " + err.Error(),
})
return
}
err = model.UpdateOption("DemoSiteEnabled", boolToString(req.DemoSiteEnabled))
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "保存演示站点模式设置失败: " + err.Error(),
})
return
}
// Update setup status
constant.Setup = true
setup := model.Setup{
Version: common.Version,
InitializedAt: time.Now().Unix(),
}
err = model.DB.Create(&setup).Error
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "系统初始化失败: " + err.Error(),
})
return
}
c.JSON(200, gin.H{
"success": true,
"message": "系统初始化成功",
})
}
func boolToString(b bool) string {
if b {
return "true"
}
return "false"
}
+7 -5
View File
@@ -913,11 +913,12 @@ func TopUp(c *gin.Context) {
}
type UpdateUserSettingRequest struct {
QuotaWarningType string `json:"notify_type"`
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
WebhookUrl string `json:"webhook_url,omitempty"`
WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"`
QuotaWarningType string `json:"notify_type"`
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
WebhookUrl string `json:"webhook_url,omitempty"`
WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"`
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
}
func UpdateUserSetting(c *gin.Context) {
@@ -993,6 +994,7 @@ func UpdateUserSetting(c *gin.Context) {
settings := map[string]interface{}{
constant.UserSettingNotifyType: req.QuotaWarningType,
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
"accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel,
}
// 如果是webhook类型,添加webhook相关设置
+24 -1
View File
@@ -3,6 +3,7 @@ package model
import (
"log"
"one-api/common"
"one-api/constant"
"os"
"strings"
"sync"
@@ -55,6 +56,26 @@ func createRootAccountIfNeed() error {
return nil
}
func checkSetup() {
if GetSetup() == nil {
if RootUserExists() {
common.SysLog("system is not initialized, but root user exists")
// Create setup record
setup := Setup{
Version: common.Version,
InitializedAt: time.Now().Unix(),
}
err := DB.Create(&setup).Error
if err != nil {
common.SysLog("failed to create setup record: " + err.Error())
}
constant.Setup = true
} else {
constant.Setup = false
}
}
}
func chooseDB(envName string) (*gorm.DB, error) {
defer func() {
initCol()
@@ -214,8 +235,10 @@ func migrateDB() error {
if err != nil {
return err
}
err = DB.AutoMigrate(&Setup{})
common.SysLog("database migrated")
err = createRootAccountIfNeed()
checkSetup()
//err = createRootAccountIfNeed()
return err
}
+16
View File
@@ -0,0 +1,16 @@
package model
type Setup struct {
ID uint `json:"id" gorm:"primaryKey"`
Version string `json:"version" gorm:"type:varchar(50);not null"`
InitializedAt int64 `json:"initialized_at" gorm:"type:bigint;not null"`
}
func GetSetup() *Setup {
var setup Setup
err := DB.First(&setup).Error
if err != nil {
return nil
}
return &setup
}
+9
View File
@@ -808,3 +808,12 @@ func (user *User) FillUserByLinuxDOId() error {
err := DB.Where("linux_do_id = ?", user.LinuxDOId).First(user).Error
return err
}
func RootUserExists() bool {
var user User
err := DB.Where("role = ?", common.RoleRootUser).First(&user).Error
if err != nil {
return false
}
return true
}
+6
View File
@@ -198,6 +198,12 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt
choice.Delta.SetReasoningContent(text + "\n")
}
} else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" {
if difyResponse.Answer == "<details style=\"color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;\" open> <summary> Thinking... </summary>\n" {
difyResponse.Answer = "<think>"
} else if difyResponse.Answer == "</details>" {
difyResponse.Answer = "</think>"
}
choice.Delta.SetContentString(difyResponse.Answer)
}
response.Choices = append(response.Choices, choice)
+14 -4
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/gin-gonic/gin"
"one-api/common"
constant2 "one-api/constant"
relaycommon "one-api/relay/common"
"one-api/setting"
"one-api/setting/operation_setting"
@@ -40,10 +41,19 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var success bool
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
if !success {
if info.UserId == 1 {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
} else {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
acceptUnsetRatio := false
if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok {
b, ok := accept.(bool)
if ok {
acceptUnsetRatio = b
}
}
if !acceptUnsetRatio {
if info.UserId == 1 {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
} else {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
}
}
}
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
+2
View File
@@ -13,6 +13,8 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
apiRouter.Use(middleware.GlobalAPIRateLimit())
{
apiRouter.GET("/setup", controller.GetSetup)
apiRouter.POST("/setup", controller.PostSetup)
apiRouter.GET("/status", controller.GetStatus)
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
+2929 -2390
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -25,6 +25,7 @@ import Task from "./pages/Task/index.js";
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
import PersonalSetting from './components/PersonalSetting.js';
import Setup from './pages/Setup/index.js';
const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
@@ -44,6 +45,14 @@ function App() {
</Suspense>
}
/>
<Route
path='/setup'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Setup />
</Suspense>
}
/>
<Route
path='/channel'
element={
File diff suppressed because it is too large Load Diff
+3
View File
@@ -60,6 +60,9 @@ export const StyleProvider = ({ children }) => {
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else if (pathname === '/setup') {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
// Only show sidebar on non-mobile devices by default
dispatch({ type: 'SET_SIDER', payload: !isMobile() });
+22 -1
View File
@@ -1346,5 +1346,26 @@
"提示缓存倍率": "Prompt cache ratio",
"缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})",
"提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}",
"缓存 Tokens": "Cache Tokens"
"缓存 Tokens": "Cache Tokens",
"系统初始化": "System initialization",
"管理员账号已经初始化过,请继续设置系统参数": "The admin account has already been initialized, please continue to set the system parameters",
"管理员账号": "Admin account",
"请输入管理员用户名": "Please enter the admin username",
"请输入管理员密码": "Please enter the admin password",
"请确认管理员密码": "Please confirm the admin password",
"请选择使用模式": "Please select the usage mode",
"数据库警告": "Database warning",
"您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "You are using the SQLite database. If you are running in a container environment, please ensure that the database file persistence mapping is correctly set, otherwise all data will be lost after container restart!",
"建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.",
"使用模式": "Usage mode",
"对外运营模式": "Default mode",
"密码长度至少为8个字符": "Password must be at least 8 characters long",
"表单引用错误,请刷新页面重试": "Form reference error, please refresh the page and try again",
"默认模式,适用于为多个用户提供服务的场景。": "Default mode, suitable for scenarios where multiple users are provided.",
"此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。": "In this mode, the system will calculate the usage of each call, you need to set the price for each model, if the price is not set, the user will not be able to use the model.",
"适用于个人使用的场景。": "Suitable for personal use.",
"不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。": "No need to set the model price, the system will weaken the usage calculation, you can focus on using the model.",
"适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
"可在初始化后修改": "Can be modified after initialization",
"初始化系统": "Initialize system"
}
+4
View File
@@ -66,6 +66,10 @@ const Home = () => {
};
useEffect(() => {
if (statusState.status?.setup === false) {
window.location.href = '/setup';
return;
}
displayNotice().then();
displayHomePageContent().then();
});
+252
View File
@@ -0,0 +1,252 @@
import React, { useContext, useEffect, useState, useRef } from 'react';
import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui';
import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons';
const Setup = () => {
const { t, i18n } = useTranslation();
const [statusState] = useContext(StatusContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const [loading, setLoading] = useState(false);
const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
const [setupStatus, setSetupStatus] = useState({
status: false,
root_init: false,
database_type: ''
});
const { Text, Title } = Typography;
const formRef = useRef(null);
const [formData, setFormData] = useState({
username: '',
password: '',
confirmPassword: '',
usageMode: 'external'
});
useEffect(() => {
fetchSetupStatus();
}, []);
const fetchSetupStatus = async () => {
try {
const res = await API.get('/api/setup');
const { success, data } = res.data;
if (success) {
setSetupStatus(data);
// If setup is already completed, redirect to home
if (data.status) {
window.location.href = '/';
}
} else {
showError(t('获取初始化状态失败'));
}
} catch (error) {
console.error('Failed to fetch setup status:', error);
showError(t('获取初始化状态失败'));
}
};
const handleUsageModeChange = (val) => {
setFormData({...formData, usageMode: val});
};
const onSubmit = () => {
if (!formRef.current) {
console.error("Form reference is null");
showError(t('表单引用错误,请刷新页面重试'));
return;
}
const values = formRef.current.getValues();
console.log("Form values:", values);
// For root_init=false, validate admin username and password
if (!setupStatus.root_init) {
if (!values.username || !values.username.trim()) {
showError(t('请输入管理员用户名'));
return;
}
if (!values.password || values.password.length < 8) {
showError(t('密码长度至少为8个字符'));
return;
}
if (values.password !== values.confirmPassword) {
showError(t('两次输入的密码不一致'));
return;
}
}
// Prepare submission data
const formValues = {...values};
formValues.SelfUseModeEnabled = values.usageMode === 'self';
formValues.DemoSiteEnabled = values.usageMode === 'demo';
// Remove usageMode as it's not needed by the backend
delete formValues.usageMode;
console.log("Submitting data to backend:", formValues);
setLoading(true);
// Submit to backend
API.post('/api/setup', formValues)
.then(res => {
const { success, message } = res.data;
console.log("API response:", res.data);
if (success) {
showNotice(t('系统初始化成功,正在跳转...'));
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showError(message || t('初始化失败,请重试'));
}
})
.catch(error => {
console.error('API error:', error);
showError(t('系统初始化失败,请重试'));
setLoading(false);
})
.finally(() => {
// setLoading(false);
});
};
return (
<>
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<Card>
<Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title>
{setupStatus.database_type === 'sqlite' && (
<Banner
type="warning"
icon={<IconAlertTriangle size="large" />}
closeIcon={null}
title={t('数据库警告')}
description={
<div>
<p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p>
<p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p>
</div>
}
style={{ marginBottom: '24px' }}
/>
)}
<Form
getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }}
initValues={formData}
>
{setupStatus.root_init ? (
<Banner
type="info"
icon={<IconInfoCircle />}
closeIcon={null}
description={t('管理员账号已经初始化过,请继续设置系统参数')}
style={{ marginBottom: '24px' }}
/>
) : (
<Form.Section text={t('管理员账号')}>
<Form.Input
field="username"
label={t('用户名')}
placeholder={t('请输入管理员用户名')}
showClear
onChange={(value) => setFormData({...formData, username: value})}
/>
<Form.Input
field="password"
label={t('密码')}
placeholder={t('请输入管理员密码')}
type="password"
showClear
onChange={(value) => setFormData({...formData, password: value})}
/>
<Form.Input
field="confirmPassword"
label={t('确认密码')}
placeholder={t('请确认管理员密码')}
type="password"
showClear
onChange={(value) => setFormData({...formData, confirmPassword: value})}
/>
</Form.Section>
)}
<Form.Section text={
<div style={{ display: 'flex', alignItems: 'center' }}>
{t('系统设置')}
</div>
}>
<Form.RadioGroup
field="usageMode"
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
{t('使用模式')}
<IconHelpCircle
style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }}
onClick={(e) => {
// e.preventDefault();
// e.stopPropagation();
setUsageModeInfoVisible(true);
}}
/>
</div>
}
extraText={t('可在初始化后修改')}
initValue="external"
onChange={handleUsageModeChange}
>
<Form.Radio value="external">{t('对外运营模式')}</Form.Radio>
<Form.Radio value="self">{t('自用模式')}</Form.Radio>
<Form.Radio value="demo">{t('演示站点模式')}</Form.Radio>
</Form.RadioGroup>
</Form.Section>
</Form>
<div style={{ marginTop: '24px', textAlign: 'right' }}>
<Button type="primary" onClick={onSubmit} loading={loading}>
{t('初始化系统')}
</Button>
</div>
</Card>
</div>
<Modal
title={t('使用模式说明')}
visible={selfUseModeInfoVisible}
onOk={() => setUsageModeInfoVisible(false)}
onCancel={() => setUsageModeInfoVisible(false)}
closeOnEsc={true}
okText={t('确定')}
cancelText={null}
>
<div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('对外运营模式')}</Title>
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
</div>
<div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('自用模式')}</Title>
<p>{t('适用于个人使用的场景。')}</p>
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
</div>
<div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('演示站点模式')}</Title>
<p>{t('适用于展示系统功能的场景。')}</p>
</div>
</Modal>
</>
);
};
export default Setup;