feat: add retry times and backup users
This commit is contained in:
@@ -641,7 +641,6 @@ table_prefix = baihu_
|
||||
| `BH_DB_NAME` | database.dbname | 数据库名称 | ql_panel |
|
||||
| `BH_DB_PATH` | database.path | SQLite 文件路径 | ./data/baihu.db |
|
||||
| `BH_DB_TABLE_PREFIX` | database.table_prefix | 表前缀 | baihu_ |
|
||||
| `BH_SECRET` | security.secret | JWT 密钥 | 手动指定 |
|
||||
|
||||
### URL 前缀配置
|
||||
|
||||
|
||||
@@ -13,8 +13,3 @@ user = root
|
||||
password =
|
||||
dbname = ql_panel
|
||||
table_prefix = baihu_
|
||||
|
||||
|
||||
[security]
|
||||
secret = baihu_secret_key_change_me
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
SectionSite = "site"
|
||||
SectionSystem = "system"
|
||||
SectionScheduler = "scheduler"
|
||||
SectionSecurity = "security"
|
||||
|
||||
// Site Settings Key 常量
|
||||
KeyTitle = "title"
|
||||
@@ -42,6 +43,9 @@ const (
|
||||
KeyCookieDays = "cookie_days"
|
||||
KeyApiToken = "api_token"
|
||||
|
||||
// Security Settings Key 常量
|
||||
KeySecret = "secret"
|
||||
|
||||
// System Settings Key 常量
|
||||
KeyInitialized = "initialized"
|
||||
|
||||
@@ -91,7 +95,7 @@ const (
|
||||
// TablePrefix 表前缀,从配置文件读取
|
||||
var TablePrefix string
|
||||
|
||||
// Secret JWT和密码salt密钥,从配置文件读取
|
||||
// Secret JWT和密码salt密钥,运行中自动从数据库加载
|
||||
var Secret string
|
||||
|
||||
// DemoMode 演示模式,从环境变量读取
|
||||
|
||||
@@ -63,8 +63,10 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
||||
CleanConfig string `json:"clean_config"`
|
||||
Envs string `json:"envs"`
|
||||
Languages []map[string]string `json:"languages"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
TriggerType string `json:"trigger_type"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
TriggerType string `json:"trigger_type"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
RetryInterval int `json:"retry_interval"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -91,7 +93,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
||||
workDir = resolveWorkDir(req.WorkDir)
|
||||
}
|
||||
|
||||
task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags)
|
||||
task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval)
|
||||
|
||||
// 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
|
||||
if task.AgentID != nil && *task.AgentID > 0 {
|
||||
@@ -166,8 +168,10 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
||||
Envs string `json:"envs"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Languages []map[string]string `json:"languages"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
TriggerType string `json:"trigger_type"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
TriggerType string `json:"trigger_type"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
RetryInterval int `json:"retry_interval"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -188,7 +192,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
||||
workDir = resolveWorkDir(req.WorkDir)
|
||||
}
|
||||
|
||||
task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags)
|
||||
task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval)
|
||||
if task == nil {
|
||||
utils.NotFound(c, "任务不存在")
|
||||
return
|
||||
|
||||
@@ -72,7 +72,13 @@ type ExecutionRequest struct {
|
||||
Timeout int // 超时时间(分钟)
|
||||
Languages []map[string]string // 语言环境配置
|
||||
UseMise bool // 是否使用 mise
|
||||
Metadata map[string]interface{} // 额外元数据
|
||||
Metadata ExecutionMetadata // 额外元数据
|
||||
}
|
||||
|
||||
// ExecutionMetadata 执行额外元数据
|
||||
type ExecutionMetadata struct {
|
||||
GoID int64 // 关联的 goroutine ID
|
||||
RetryIndex int // 当前重试索引
|
||||
}
|
||||
|
||||
// ExecutionResult 执行结果(标准接口)
|
||||
@@ -275,6 +281,21 @@ func (s *Scheduler) EnqueueOrExecute(req *ExecutionRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// EnqueueDelayed 延迟将任务加入队列执行
|
||||
func (s *Scheduler) EnqueueDelayed(delay time.Duration, reqBuilder func() *ExecutionRequest) {
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
if req := reqBuilder(); req != nil {
|
||||
s.EnqueueOrExecute(req)
|
||||
}
|
||||
case <-s.stopCh:
|
||||
// 调度器停止时取消延迟投递
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ExecuteSync 同步执行任务(不经过队列)
|
||||
func (s *Scheduler) ExecuteSync(req *ExecutionRequest) (*ExecutionResult, error) {
|
||||
return s.executeTask(req)
|
||||
|
||||
+10
-8
@@ -47,14 +47,16 @@ type Task struct {
|
||||
CleanConfig string `json:"clean_config" gorm:"size:255;default:''"` // 清理配置 JSON
|
||||
Envs string `json:"envs" gorm:"size:255;default:''"` // 环境变量ID列表,逗号分隔
|
||||
Languages []map[string]string `json:"languages" gorm:"serializer:json;type:text"` // 针对本地任务的语言配置列表
|
||||
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
RunningGo string `json:"running_go" gorm:"type:text"` // 正在运行的 go routine id 数组 (JSON)
|
||||
LastRun *LocalTime `json:"last_run"`
|
||||
NextRun *LocalTime `json:"next_run"`
|
||||
CreatedAt LocalTime `json:"created_at"`
|
||||
UpdatedAt LocalTime `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
||||
RetryCount int `json:"retry_count" gorm:"default:0"` // 失败重试次数
|
||||
RetryInterval int `json:"retry_interval" gorm:"default:0"` // 失败重试间隔(秒)
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
RunningGo string `json:"running_go" gorm:"type:text"` // 正在运行的 go routine id 数组 (JSON)
|
||||
LastRun *LocalTime `json:"last_run"`
|
||||
NextRun *LocalTime `json:"next_run"`
|
||||
CreatedAt LocalTime `json:"created_at"`
|
||||
UpdatedAt LocalTime `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
func (Task) TableName() string {
|
||||
|
||||
@@ -21,8 +21,10 @@ type TaskVO struct {
|
||||
Envs string `json:"envs"`
|
||||
Languages []map[string]string `json:"languages"`
|
||||
AgentID *uint `json:"agent_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LastRun *models.LocalTime `json:"last_run"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
RetryInterval int `json:"retry_interval"`
|
||||
LastRun *models.LocalTime `json:"last_run"`
|
||||
NextRun *models.LocalTime `json:"next_run"`
|
||||
CreatedAt models.LocalTime `json:"created_at"`
|
||||
UpdatedAt models.LocalTime `json:"updated_at"`
|
||||
@@ -47,9 +49,11 @@ func ToTaskVO(task *models.Task) *TaskVO {
|
||||
CleanConfig: task.CleanConfig,
|
||||
Envs: task.Envs,
|
||||
Languages: task.Languages,
|
||||
AgentID: task.AgentID,
|
||||
Enabled: task.Enabled,
|
||||
LastRun: task.LastRun,
|
||||
AgentID: task.AgentID,
|
||||
Enabled: task.Enabled,
|
||||
RetryCount: task.RetryCount,
|
||||
RetryInterval: task.RetryInterval,
|
||||
LastRun: task.LastRun,
|
||||
NextRun: task.NextRun,
|
||||
CreatedAt: task.CreatedAt,
|
||||
UpdatedAt: task.UpdatedAt,
|
||||
|
||||
@@ -42,6 +42,7 @@ type tableConfig struct {
|
||||
|
||||
func (s *BackupService) getTableConfigs() []tableConfig {
|
||||
return []tableConfig{
|
||||
{"users.json", s.exportTable(&[]models.User{}, true), s.restoreTable(&[]models.User{}, true)},
|
||||
{"tasks.json", s.exportTable(&[]models.Task{}, true), s.restoreTable(&[]models.Task{}, true)},
|
||||
{"task_logs.json", s.exportTable(&[]models.TaskLog{}, false), s.restoreTable(&[]models.TaskLog{}, false)},
|
||||
{"envs.json", s.exportTable(&[]models.EnvironmentVariable{}, true), s.restoreTable(&[]models.EnvironmentVariable{}, true)},
|
||||
@@ -199,6 +200,7 @@ func (s *BackupService) Restore(zipPath string) error {
|
||||
// 开启全局事务
|
||||
return database.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 清空现有数据(物理删除)
|
||||
tx.Unscoped().Where("1=1").Delete(&models.User{})
|
||||
tx.Unscoped().Where("1=1").Delete(&models.Task{})
|
||||
tx.Unscoped().Where("1=1").Delete(&models.TaskLog{})
|
||||
tx.Unscoped().Where("1=1").Delete(&models.EnvironmentVariable{})
|
||||
@@ -281,6 +283,8 @@ func (s *BackupService) restoreFromZipFile(tx *gorm.DB, f *zip.File, filename st
|
||||
}
|
||||
|
||||
switch filename {
|
||||
case "users.json":
|
||||
return restoreStreamBatch[models.User](tx, decoder)
|
||||
case "tasks.json":
|
||||
return restoreStreamBatch[models.Task](tx, decoder)
|
||||
case "task_logs.json":
|
||||
|
||||
@@ -102,8 +102,8 @@ func LoadConfig(path string) (*AppConfig, error) {
|
||||
// 设置表前缀到 constant 包
|
||||
constant.TablePrefix = Config.Database.TablePrefix
|
||||
|
||||
// 设置 Secret 到 constant 包
|
||||
constant.Secret = Config.Security.Secret
|
||||
// 暂存旧的 Secret,不再直接给 constant 赋值(改为到 settings 初始化时判断)
|
||||
// constant.Secret = Config.Security.Secret
|
||||
|
||||
// 设置演示模式
|
||||
if v := os.Getenv("BH_DEMO_MODE"); v == "true" || v == "1" {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/engigu/baihu-panel/internal/database"
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
)
|
||||
|
||||
type SettingsService struct{}
|
||||
@@ -26,6 +27,25 @@ func (s *SettingsService) InitSettings() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 初始化或获取 JWT Secret 密码
|
||||
var secCount int64
|
||||
database.DB.Model(&models.Setting{}).Where("section = ? AND `key` = ?", constant.SectionSecurity, constant.KeySecret).Count(&secCount)
|
||||
var secretValue string
|
||||
if secCount == 0 {
|
||||
// 先尝试从配置文件读取遗留下来的旧设
|
||||
if Config != nil && Config.Security.Secret != "" {
|
||||
secretValue = Config.Security.Secret
|
||||
} else {
|
||||
secretValue = utils.RandomString(32)
|
||||
}
|
||||
if err := database.DB.Create(&models.Setting{Section: constant.SectionSecurity, Key: constant.KeySecret, Value: secretValue}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
secretValue = s.Get(constant.SectionSecurity, constant.KeySecret)
|
||||
}
|
||||
constant.Secret = secretValue
|
||||
|
||||
cache.LoadSiteCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -141,10 +141,7 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest)
|
||||
return nil, nil, fmt.Errorf("任务并发限制: %v", err)
|
||||
}
|
||||
|
||||
if req.Metadata == nil {
|
||||
req.Metadata = make(map[string]interface{})
|
||||
}
|
||||
req.Metadata["goid"] = goid
|
||||
req.Metadata.GoID = goid
|
||||
|
||||
// 3. 创建 TinyLog 实时日志收集器
|
||||
tl, err := NewTinyLog(taskLog.ID)
|
||||
@@ -161,6 +158,10 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest)
|
||||
StartTime: time.Now(),
|
||||
})
|
||||
|
||||
if req.Metadata.RetryIndex > 0 {
|
||||
tl.Write([]byte(fmt.Sprintf("\n[System] 此为任务失败后的第 %d 次重试执行...\n\n", req.Metadata.RetryIndex)))
|
||||
}
|
||||
|
||||
// 对于本地任务,Scheduler 会通过返回的 Writer 写入日志
|
||||
// 对于远程任务,Scheduler 不会写入任何内容(由 Agent 推送至此 TL)
|
||||
return tl, tl, nil
|
||||
@@ -235,10 +236,8 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
||||
}
|
||||
|
||||
// 移除运行记录
|
||||
if req.Metadata != nil {
|
||||
if goid, ok := req.Metadata["goid"].(int64); ok {
|
||||
h.es.RemoveRunningGo(task.ID, goid)
|
||||
}
|
||||
if req.Metadata.GoID != 0 {
|
||||
h.es.RemoveRunningGo(task.ID, req.Metadata.GoID)
|
||||
}
|
||||
|
||||
// 处理任务完成(更新统计、清理旧日志等)
|
||||
@@ -246,6 +245,9 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
||||
|
||||
// 更新内存缓冲
|
||||
h.es.UpdateResult(*result)
|
||||
|
||||
// ======= 重试逻辑 =======
|
||||
h.es.HandleTaskRetry(task, req, result.Success, result.Status, result.ExitCode)
|
||||
}
|
||||
|
||||
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
|
||||
@@ -257,10 +259,8 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
|
||||
fmt.Sscanf(req.TaskID, "%d", &taskID)
|
||||
|
||||
// 移除运行记录
|
||||
if req.Metadata != nil {
|
||||
if goid, ok := req.Metadata["goid"].(int64); ok {
|
||||
h.es.RemoveRunningGo(taskID, goid)
|
||||
}
|
||||
if req.Metadata.GoID != 0 {
|
||||
h.es.RemoveRunningGo(taskID, req.Metadata.GoID)
|
||||
}
|
||||
|
||||
// 构造错误日志
|
||||
@@ -305,6 +305,48 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
|
||||
StartTime: time.Now(),
|
||||
EndTime: time.Now(),
|
||||
})
|
||||
|
||||
// ======= 重试逻辑 =======
|
||||
h.es.HandleTaskRetry(task, req, false, constant.TaskStatusFailed, 1)
|
||||
}
|
||||
|
||||
// HandleTaskRetry 处理任务失败重试逻辑
|
||||
func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.ExecutionRequest, isSuccess bool, status string, exitCode int) {
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !isSuccess || status == constant.TaskStatusFailed || status == constant.TaskStatusTimeout || exitCode != 0 {
|
||||
retryIndex := req.Metadata.RetryIndex
|
||||
|
||||
if retryIndex < task.RetryCount {
|
||||
retryIndex++
|
||||
logger.Infof("[Executor] 任务 #%d 执行失败/出错,将在 %d 秒后进行第 %d/%d 次重试...", task.ID, task.RetryInterval, retryIndex, task.RetryCount)
|
||||
|
||||
es.scheduler.EnqueueDelayed(time.Duration(task.RetryInterval)*time.Second, func() *executor.ExecutionRequest {
|
||||
latestTask := es.taskService.GetTaskByID(int(task.ID))
|
||||
if latestTask == nil || !latestTask.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
newEnvs := es.loadEnvVars(latestTask.Envs)
|
||||
return &executor.ExecutionRequest{
|
||||
TaskID: req.TaskID,
|
||||
Name: latestTask.Name,
|
||||
Command: latestTask.Command,
|
||||
WorkDir: latestTask.WorkDir,
|
||||
Envs: newEnvs,
|
||||
Timeout: latestTask.Timeout,
|
||||
Languages: latestTask.Languages,
|
||||
UseMise: latestTask.UseMise(),
|
||||
Type: executor.TaskTypeManual,
|
||||
Metadata: executor.ExecutionMetadata{
|
||||
RetryIndex: retryIndex,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ServerSchedulerHandler) OnCronNextRun(req *executor.ExecutionRequest, nextRun time.Time) {
|
||||
|
||||
@@ -12,7 +12,7 @@ func NewTaskService() *TaskService {
|
||||
return &TaskService{}
|
||||
}
|
||||
|
||||
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string) *models.Task {
|
||||
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int) *models.Task {
|
||||
if taskType == "" {
|
||||
taskType = "task"
|
||||
}
|
||||
@@ -31,9 +31,11 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w
|
||||
WorkDir: workDir,
|
||||
CleanConfig: cleanConfig,
|
||||
Envs: envs,
|
||||
Languages: languages,
|
||||
AgentID: agentID,
|
||||
Enabled: true,
|
||||
Languages: languages,
|
||||
AgentID: agentID,
|
||||
Enabled: true,
|
||||
RetryCount: retryCount,
|
||||
RetryInterval: retryInterval,
|
||||
}
|
||||
if triggerType != constant.TriggerTypeCron {
|
||||
task.NextRun = nil
|
||||
@@ -81,7 +83,7 @@ func (ts *TaskService) GetTaskByID(id int) *models.Task {
|
||||
return &task
|
||||
}
|
||||
|
||||
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string) *models.Task {
|
||||
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int) *models.Task {
|
||||
var task models.Task
|
||||
if err := database.DB.First(&task, id).Error; err != nil {
|
||||
return nil
|
||||
@@ -97,6 +99,8 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeou
|
||||
task.Enabled = enabled
|
||||
task.AgentID = agentID
|
||||
task.Languages = languages
|
||||
task.RetryCount = retryCount
|
||||
task.RetryInterval = retryInterval
|
||||
if taskType != "" {
|
||||
task.Type = taskType
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ export interface Task {
|
||||
work_dir: string
|
||||
clean_config: string
|
||||
envs: string
|
||||
retry_count: number
|
||||
retry_interval: number
|
||||
languages: { name: string; version: string }[]
|
||||
agent_id: number | null
|
||||
enabled: boolean
|
||||
|
||||
@@ -349,6 +349,20 @@ async function save() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-3">
|
||||
<Label class="sm:text-right text-sm">失败重试</Label>
|
||||
<div class="sm:col-span-3 flex items-center gap-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Input v-model.number="form.retry_count" type="number" placeholder="0" class="w-16 h-9 text-sm" />
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">次</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 ml-2" v-if="form.retry_count && form.retry_count > 0">
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">间隔</span>
|
||||
<Input v-model.number="form.retry_interval" type="number" placeholder="0" class="w-16 h-9 text-sm" />
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter class="shrink-0 p-6 pt-3 border-t">
|
||||
<Button variant="outline" size="sm" @click="emit('update:open', false)">取消</Button>
|
||||
|
||||
@@ -605,6 +605,20 @@ async function save() {
|
||||
<p class="text-xs text-muted-foreground">如果任务未执行完成,是否允许再次执行</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-3">
|
||||
<Label class="sm:text-right text-sm">失败重试</Label>
|
||||
<div class="sm:col-span-3 flex items-center gap-2">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Input v-model.number="form.retry_count" type="number" placeholder="0" class="w-16 h-9 text-sm" />
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">次</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 ml-2" v-if="form.retry_count && form.retry_count > 0">
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">间隔</span>
|
||||
<Input v-model.number="form.retry_interval" type="number" placeholder="0" class="w-16 h-9 text-sm" />
|
||||
<span class="text-sm text-muted-foreground whitespace-nowrap">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-start gap-2 sm:gap-3">
|
||||
<Label class="sm:text-right text-sm pt-1.5">环境变量</Label>
|
||||
<div class="sm:col-span-3 space-y-1.5">
|
||||
|
||||
Reference in New Issue
Block a user