Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a37d83a7 | |||
| a1f6781c0e | |||
| 88f7c0670f | |||
| c08edc315d | |||
| 683accf05b | |||
| d8c10dcb51 | |||
| 3e55cbcda2 | |||
| 07d31760da | |||
| f33ebc8e2c | |||
| 1de0d9123b | |||
| f0925dc105 | |||
| d9223147f2 | |||
| 111cedf795 |
@@ -117,7 +117,6 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
|
||||
> [!TIP]
|
||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||
> 默认账号root 密码123456
|
||||
|
||||
### 多机部署注意事项
|
||||
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package constant
|
||||
|
||||
var Setup = false
|
||||
@@ -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 (
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+2929
-2390
File diff suppressed because it is too large
Load Diff
@@ -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={
|
||||
|
||||
+1016
-985
File diff suppressed because it is too large
Load Diff
@@ -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() });
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ const Home = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (statusState.status?.setup === false) {
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
displayNotice().then();
|
||||
displayHomePageContent().then();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user