From a882e680ae662d7f425ae97b10fd7887c44cb25e Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Thu, 3 Apr 2025 18:57:15 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20Implement=20system=20se?= =?UTF-8?q?tup=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- constant/setup.go | 3 + controller/misc.go | 2 + controller/setup.go | 167 ++++++++++++++++++++++ model/main.go | 25 +++- model/setup.go | 16 +++ model/user.go | 9 ++ router/api-router.go | 2 + web/src/App.js | 9 ++ web/src/context/Style/index.js | 3 + web/src/pages/Home/index.js | 4 + web/src/pages/Setup/index.js | 251 +++++++++++++++++++++++++++++++++ 11 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 constant/setup.go create mode 100644 controller/setup.go create mode 100644 model/setup.go create mode 100644 web/src/pages/Setup/index.js diff --git a/constant/setup.go b/constant/setup.go new file mode 100644 index 00000000..26ecc883 --- /dev/null +++ b/constant/setup.go @@ -0,0 +1,3 @@ +package constant + +var Setup = false diff --git a/controller/misc.go b/controller/misc.go index aff33a31..4d265c3f 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -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 diff --git a/controller/setup.go b/controller/setup.go new file mode 100644 index 00000000..59f1d47f --- /dev/null +++ b/controller/setup.go @@ -0,0 +1,167 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "one-api/common" + "one-api/constant" + "one-api/model" + "one-api/setting/operation_setting" +) + +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("self_use_mode", boolToString(req.SelfUseModeEnabled)) + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "保存自用模式设置失败: " + err.Error(), + }) + return + } + + err = model.UpdateOption("demo_site_mode", boolToString(req.DemoSiteEnabled)) + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "保存演示站点模式设置失败: " + err.Error(), + }) + return + } + + // Update setup status + constant.Setup = true + err = model.UpdateOption("setup", "true") + 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" +} diff --git a/model/main.go b/model/main.go index 649dac54..a759495b 100644 --- a/model/main.go +++ b/model/main.go @@ -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 } diff --git a/model/setup.go b/model/setup.go new file mode 100644 index 00000000..c4d7997f --- /dev/null +++ b/model/setup.go @@ -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 +} diff --git a/model/user.go b/model/user.go index 523ca4ee..c15e5370 100644 --- a/model/user.go +++ b/model/user.go @@ -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 +} diff --git a/router/api-router.go b/router/api-router.go index 0dbabb5f..1720ff57 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -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) diff --git a/web/src/App.js b/web/src/App.js index c9dd8ece..15f449be 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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() { } /> + } key={location.pathname}> + + + } + /> { 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() }); diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index dce2bb88..d6a2de71 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -66,6 +66,10 @@ const Home = () => { }; useEffect(() => { + if (statusState.status?.setup === false) { + window.location.href = '/setup'; + return; + } displayNotice().then(); displayHomePageContent().then(); }); diff --git a/web/src/pages/Setup/index.js b/web/src/pages/Setup/index.js new file mode 100644 index 00000000..4d574272 --- /dev/null +++ b/web/src/pages/Setup/index.js @@ -0,0 +1,251 @@ +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 ( + <> +
+ + {t('系统初始化')} + + {setupStatus.database_type === 'sqlite' && ( + } + closeIcon={null} + title={t('数据库警告')} + description={ +
+

{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}

+

{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}

+
+ } + style={{ marginBottom: '24px' }} + /> + )} + +
{ formRef.current = formApi; console.log("Form API set:", formApi); }} + initValues={formData} + > + {setupStatus.root_init ? ( + } + closeIcon={null} + description={t('管理员账号已经初始化过,请继续设置系统参数')} + style={{ marginBottom: '24px' }} + /> + ) : ( + + setFormData({...formData, username: value})} + /> + setFormData({...formData, password: value})} + /> + setFormData({...formData, confirmPassword: value})} + /> + + )} + + + {t('系统设置')} +
+ }> + + {t('使用模式')} + { + // e.preventDefault(); + // e.stopPropagation(); + setUsageModeInfoVisible(true); + }} + /> + + } + extraText={t('可在初始化后修改')} + initValue="external" + onChange={handleUsageModeChange} + > + {t('对外运营模式')} + {t('自用模式')} + {t('演示站点模式')} + + + + +
+ +
+ + + + setUsageModeInfoVisible(false)} + onCancel={() => setUsageModeInfoVisible(false)} + closeOnEsc={true} + okText={t('确定')} + cancelText={null} + > +
+ {t('对外运营模式')} +

{t('默认模式,适用于为多个用户提供服务的场景。')}

+

{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}

+
+
+ {t('自用模式')} +

{t('适用于个人使用的场景。不需要设置模型价格,您可专注于使用模型。')}

+
+
+ {t('演示站点模式')} +

{t('适用于展示系统功能的场景。')}

+
+
+ + ); +}; + +export default Setup; From 5fa6462412d519a117c6e9af2fa3e8c65ad283d3 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Thu, 3 Apr 2025 19:01:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20feat:=20Refine=20personal=20mod?= =?UTF-8?q?e=20description=20in=20setup=20page=20for=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Setup/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Setup/index.js b/web/src/pages/Setup/index.js index 4d574272..82798f70 100644 --- a/web/src/pages/Setup/index.js +++ b/web/src/pages/Setup/index.js @@ -237,7 +237,8 @@ const Setup = () => {
{t('自用模式')} -

{t('适用于个人使用的场景。不需要设置模型价格,您可专注于使用模型。')}

+

{t('适用于个人使用的场景。')}

+

{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}

{t('演示站点模式')}