diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 3f14c99..f3b1df0 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -56,6 +56,9 @@ var TablePrefix string // Secret JWT和密码salt密钥,从配置文件读取 var Secret string +// DemoMode 演示模式,从环境变量读取 +var DemoMode bool + // DefaultIcon 默认站点图标 var DefaultIcon = `` diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go index 022b9f0..e9bf534 100644 --- a/internal/controllers/settings_controller.go +++ b/internal/controllers/settings_controller.go @@ -37,6 +37,12 @@ func NewSettingsController(userService *services.UserService, loginLogService *s // ChangePassword 修改密码 func (sc *SettingsController) ChangePassword(c *gin.Context) { + // 演示模式下禁止修改密码 + if constant.DemoMode { + utils.BadRequest(c, "演示模式下不能修改密码") + return + } + var req struct { OldPassword string `json:"old_password" binding:"required"` NewPassword string `json:"new_password" binding:"required,min=6"` @@ -83,6 +89,7 @@ func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) { constant.KeyTitle: settings[constant.KeyTitle], constant.KeySubtitle: settings[constant.KeySubtitle], constant.KeyIcon: settings[constant.KeyIcon], + "demo_mode": constant.DemoMode, }) } diff --git a/internal/controllers/terminal_controller.go b/internal/controllers/terminal_controller.go index 6400280..8d012b5 100644 --- a/internal/controllers/terminal_controller.go +++ b/internal/controllers/terminal_controller.go @@ -72,6 +72,12 @@ func (tc *TerminalController) HandleWebSocket(c *gin.Context) { } defer conn.Close() + // 演示模式下禁用终端 + if constant.DemoMode { + conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[1;33m[演示模式] 终端功能已禁用\033[0m\r\n")) + return + } + // Windows 使用 pipe 模式,Unix 使用 PTY 模式 if runtime.GOOS == "windows" { tc.handlePipeMode(conn) diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 4f69422..ff50861 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -103,6 +103,12 @@ func LoadConfig(path string) (*AppConfig, error) { // 设置 Secret 到 constant 包 constant.Secret = Config.Security.Secret + // 设置演示模式 + if v := os.Getenv("BH_DEMO_MODE"); v == "true" || v == "1" { + constant.DemoMode = true + log.Printf("[Config] Demo mode enabled") + } + // 输出配置信息(隐藏敏感信息) log.Printf("[Config] Server: %s:%d", Config.Server.Host, Config.Server.Port) log.Printf("[Config] Database: type=%s, host=%s, port=%d, dbname=%s", diff --git a/internal/services/cron_service.go b/internal/services/cron_service.go index 4347645..c59e913 100644 --- a/internal/services/cron_service.go +++ b/internal/services/cron_service.go @@ -4,6 +4,7 @@ import ( "sync" "time" + "baihu/internal/constant" "baihu/internal/database" "baihu/internal/logger" "baihu/internal/models" diff --git a/internal/services/executor_service.go b/internal/services/executor_service.go index ad8744f..7144f40 100644 --- a/internal/services/executor_service.go +++ b/internal/services/executor_service.go @@ -322,6 +322,11 @@ func (es *ExecutorService) executeTaskInternal(taskID int) *ExecutionResult { // executeNormalTask 执行普通任务 func (es *ExecutorService) executeNormalTask(task *models.Task) *ExecutionResult { + // 演示模式下使用 echo 替换实际命令 + if constant.DemoMode { + return es.ExecuteCommandWithOptions("echo '[演示模式] 任务执行已跳过,实际命令不会运行'", time.Minute, nil, "") + } + // 加载环境变量 envService := NewEnvService() envVars := envService.GetEnvVarsByIDs(task.Envs) @@ -342,6 +347,11 @@ func (es *ExecutorService) executeNormalTask(task *models.Task) *ExecutionResult // executeRepoTask 执行仓库同步任务(调用 sync.py) func (es *ExecutorService) executeRepoTask(task *models.Task) *ExecutionResult { + // 演示模式下使用 echo 替换实际命令 + if constant.DemoMode { + return es.ExecuteCommandWithOptions("echo '[演示模式] 仓库同步已跳过,实际命令不会运行'", time.Minute, nil, "") + } + result := &ExecutionResult{ Success: false, Start: time.Now(), diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 94ff8d1..09c60db 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -111,7 +111,7 @@ export const api = { changePassword: (data: { old_password: string; new_password: string }) => request('/settings/password', { method: 'POST', body: JSON.stringify(data) }), getSite: () => request('/settings/site'), - getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'), + getPublicSite: () => request<{ title: string; subtitle: string; icon: string; demo_mode: boolean }>('/settings/public'), updateSite: (data: SiteSettings) => request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }), getScheduler: () => request('/settings/scheduler'), diff --git a/web/src/views/login/Login.vue b/web/src/views/login/Login.vue index 40e025b..7025790 100644 --- a/web/src/views/login/Login.vue +++ b/web/src/views/login/Login.vue @@ -17,6 +17,7 @@ const loading = ref(false) const siteTitle = ref('白虎面板') const siteSubtitle = ref('轻量级定时任务管理系统') const siteIcon = ref('') +const demoMode = ref(false) async function loadSiteSettings() { try { @@ -24,7 +25,13 @@ async function loadSiteSettings() { siteTitle.value = res.title || '白虎面板' siteSubtitle.value = res.subtitle || '轻量级定时任务管理系统' siteIcon.value = res.icon || '' + demoMode.value = res.demo_mode || false document.title = siteTitle.value + // 演示模式下自动填充账号密码 + if (demoMode.value) { + username.value = 'admin' + password.value = '123456' + } // 设置 favicon if (siteIcon.value && siteIcon.value.trim().startsWith(' -import { ref } from 'vue' +import { ref, onMounted } from 'vue' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { AlertTriangle } from 'lucide-vue-next' import { api } from '@/api' import { toast } from 'vue-sonner' const oldPassword = ref('') const newPassword = ref('') const confirmPassword = ref('') +const demoMode = ref(false) + +async function loadDemoMode() { + try { + const res = await api.settings.getPublicSite() + demoMode.value = res.demo_mode || false + } catch { + // ignore + } +} async function changePassword() { + if (demoMode.value) { + toast.error('演示模式下不能修改密码') + return + } if (!oldPassword.value || !newPassword.value) { toast.error('请填写完整') return @@ -33,24 +48,30 @@ async function changePassword() { toast.error(e.message || '修改失败') } } + +onMounted(loadDemoMode)