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('