feat: implement agent custom scheduler config and queue model #101
This commit is contained in:
+33
-12
@@ -152,6 +152,7 @@ type Agent struct {
|
||||
wsStopCh chan struct{} // 用于停止当前 WebSocket 相关的 goroutine
|
||||
taskLogs map[string][]string // 记录最近的日志行,用于失败显示
|
||||
logMu sync.Mutex // taskLogs 的锁
|
||||
schedulerStarted bool // 调度器是否已经启动
|
||||
}
|
||||
|
||||
func NewAgent(config *Config, configFile string) *Agent {
|
||||
@@ -267,9 +268,7 @@ func (a *Agent) Start() error {
|
||||
}
|
||||
|
||||
logger.Infof("机器识别码: %s", a.machineID[:16]+"...")
|
||||
a.scheduler.Start()
|
||||
a.cronManager.Start()
|
||||
|
||||
// 调度器暂不在此启动,等待 WebSocket 连接成功并获取到调度配置后再启动
|
||||
go a.wsLoop()
|
||||
|
||||
logger.Info("Agent 已启动 (时区: Asia/Shanghai, 模式: WebSocket)")
|
||||
@@ -279,8 +278,16 @@ func (a *Agent) Start() error {
|
||||
func (a *Agent) Stop() {
|
||||
close(a.stopCh)
|
||||
a.closeWS()
|
||||
a.cronManager.Stop()
|
||||
a.scheduler.Stop()
|
||||
|
||||
a.mu.Lock()
|
||||
started := a.schedulerStarted
|
||||
a.schedulerStarted = false
|
||||
a.mu.Unlock()
|
||||
|
||||
if started {
|
||||
a.cronManager.Stop()
|
||||
a.scheduler.Stop()
|
||||
}
|
||||
logger.Info("Agent 已停止")
|
||||
}
|
||||
|
||||
@@ -460,16 +467,30 @@ func (a *Agent) updateSchedulerConfig(config map[string]interface{}) {
|
||||
newCfg.RateInterval = time.Duration(v) * time.Millisecond
|
||||
}
|
||||
}
|
||||
if val, ok := config["strict_queue"]; ok {
|
||||
if v, ok := val.(bool); ok {
|
||||
newCfg.StrictQueue = v
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当配置发生变化时才重新加载
|
||||
// 只有当配置发生变化时才重新加载
|
||||
if newCfg != currentCfg {
|
||||
logger.Infof("收到调度配置更新: workers=%d, queue=%d, rate=%v",
|
||||
newCfg.WorkerCount, newCfg.QueueSize, newCfg.RateInterval)
|
||||
a.mu.Lock()
|
||||
started := a.schedulerStarted
|
||||
a.schedulerStarted = true
|
||||
a.mu.Unlock()
|
||||
|
||||
if !started {
|
||||
logger.Infof("首次连接成功,启动调度器配置: workers=%d, queue=%d, rate=%v, strict=%t",
|
||||
newCfg.WorkerCount, newCfg.QueueSize, newCfg.RateInterval, newCfg.StrictQueue)
|
||||
// 用下发的最新配置加载并启动调度器与计划任务管理器
|
||||
a.scheduler.Reload(newCfg)
|
||||
a.cronManager.Start()
|
||||
} else if newCfg != currentCfg {
|
||||
logger.Infof("收到调度配置更新: workers=%d, queue=%d, rate=%v, strict=%t",
|
||||
newCfg.WorkerCount, newCfg.QueueSize, newCfg.RateInterval, newCfg.StrictQueue)
|
||||
a.scheduler.Reload(newCfg)
|
||||
} else {
|
||||
logger.Infof("当前调度配置: workers=%d, queue=%d, rate=%v",
|
||||
newCfg.WorkerCount, newCfg.QueueSize, newCfg.RateInterval)
|
||||
logger.Infof("当前调度配置未改变: workers=%d, queue=%d, rate=%v, strict=%t",
|
||||
newCfg.WorkerCount, newCfg.QueueSize, newCfg.RateInterval, newCfg.StrictQueue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,29 @@ func (c *AgentController) List(ctx *gin.Context) {
|
||||
utils.Success(ctx, vo.ToAgentVOListFromModels(agents))
|
||||
}
|
||||
|
||||
// getActiveSchedulerConfig 获取 Agent 的实际调度配置(若为空或零值,则使用系统默认的 settings)
|
||||
func (c *AgentController) getActiveSchedulerConfig(agent *models.Agent) map[string]interface{} {
|
||||
workerCount := agent.SchedulerConfig.WorkerCount
|
||||
queueSize := agent.SchedulerConfig.QueueSize
|
||||
rateInterval := int(agent.SchedulerConfig.RateInterval / time.Millisecond)
|
||||
strictQueue := agent.SchedulerConfig.StrictQueue
|
||||
|
||||
// 如果未配置(WorkerCount <= 0),则使用全局系统设置
|
||||
if workerCount <= 0 {
|
||||
workerCount = getIntSetting(c.settingsService, constant.SectionScheduler, constant.KeyWorkerCount, 4)
|
||||
queueSize = getIntSetting(c.settingsService, constant.SectionScheduler, constant.KeyQueueSize, 100)
|
||||
rateInterval = getIntSetting(c.settingsService, constant.SectionScheduler, constant.KeyRateInterval, 200)
|
||||
strictQueue = false
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"worker_count": workerCount,
|
||||
"queue_size": queueSize,
|
||||
"rate_interval": rateInterval,
|
||||
"strict_queue": strictQueue,
|
||||
}
|
||||
}
|
||||
|
||||
// Update 更新 Agent
|
||||
func (c *AgentController) Update(ctx *gin.Context) {
|
||||
id := ctx.Param("id")
|
||||
@@ -54,16 +77,17 @@ func (c *AgentController) Update(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SchedulerConfig *vo.AgentSchedulerConfigVO `json:"scheduler_config"`
|
||||
}
|
||||
|
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(ctx, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 获取旧状态
|
||||
oldAgent := c.agentService.GetByID(id)
|
||||
if oldAgent == nil {
|
||||
@@ -71,12 +95,21 @@ func (c *AgentController) Update(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
wasEnabled := utils.DerefBool(oldAgent.Enabled, true)
|
||||
|
||||
var schedulerConfig models.AgentSchedulerConfig
|
||||
if req.SchedulerConfig != nil {
|
||||
schedulerConfig.WorkerCount = req.SchedulerConfig.WorkerCount
|
||||
schedulerConfig.QueueSize = req.SchedulerConfig.QueueSize
|
||||
schedulerConfig.RateInterval = time.Duration(req.SchedulerConfig.RateInterval) * time.Millisecond
|
||||
schedulerConfig.Verbose = req.SchedulerConfig.Verbose
|
||||
schedulerConfig.StrictQueue = req.SchedulerConfig.StrictQueue
|
||||
}
|
||||
|
||||
if err := c.agentService.Update(id, req.Name, req.Description, req.Enabled); err != nil {
|
||||
if err := c.agentService.Update(id, req.Name, req.Description, req.Enabled, schedulerConfig); err != nil {
|
||||
utils.ServerError(ctx, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果启用状态发生变化,通知 Agent
|
||||
if wasEnabled != req.Enabled {
|
||||
if req.Enabled {
|
||||
@@ -93,7 +126,20 @@ func (c *AgentController) Update(ctx *gin.Context) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 推送最新的调度配置给 Agent (如果 Agent 在线)
|
||||
if req.Enabled {
|
||||
// 重新加载已更新的 Agent 信息以获取正确的 SchedulerConfig
|
||||
updatedAgent := c.agentService.GetByID(id)
|
||||
if updatedAgent != nil {
|
||||
c.wsManager.SendToAgent(id, services.WSTypeConnected, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
"name": req.Name,
|
||||
"scheduler_config": c.getActiveSchedulerConfig(updatedAgent),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
utils.SuccessMsg(ctx, "更新成功")
|
||||
}
|
||||
|
||||
@@ -415,26 +461,17 @@ func (c *AgentController) WSConnect(ctx *gin.Context) {
|
||||
// 更新 Agent 状态
|
||||
c.agentService.Heartbeat(token, ip, "", "", "", "", "")
|
||||
|
||||
// 获取调度配置
|
||||
workerCount := getIntSetting(c.settingsService, constant.SectionScheduler, constant.KeyWorkerCount, 4)
|
||||
queueSize := getIntSetting(c.settingsService, constant.SectionScheduler, constant.KeyQueueSize, 100)
|
||||
rateInterval := getIntSetting(c.settingsService, constant.SectionScheduler, constant.KeyRateInterval, 200)
|
||||
|
||||
// 发送连接成功消息(包含注册状态和调度配置)
|
||||
// 获取调度配置并发送连接成功消息(包含注册状态和调度配置)
|
||||
schedCfg := c.getActiveSchedulerConfig(agent)
|
||||
c.wsManager.SendToAgent(agent.ID, services.WSTypeConnected, map[string]interface{}{
|
||||
"agent_id": agent.ID,
|
||||
"name": agent.Name,
|
||||
"is_new_agent": isNewAgent,
|
||||
"machine_id": machineID,
|
||||
"scheduler_config": map[string]interface{}{
|
||||
"worker_count": workerCount,
|
||||
"queue_size": queueSize,
|
||||
"rate_interval": rateInterval,
|
||||
},
|
||||
"agent_id": agent.ID,
|
||||
"name": agent.Name,
|
||||
"is_new_agent": isNewAgent,
|
||||
"machine_id": machineID,
|
||||
"scheduler_config": schedCfg,
|
||||
})
|
||||
|
||||
logger.Infof("[AgentWS] Agent #%s 连接成功 (配置: workers=%d, queue=%d, rate=%d)",
|
||||
agent.ID, workerCount, queueSize, rateInterval)
|
||||
|
||||
logger.Infof("[AgentWS] Agent #%s 连接成功 (配置: %v)", agent.ID, schedCfg)
|
||||
|
||||
// 启动读写协程
|
||||
go c.wsWritePump(ac)
|
||||
|
||||
@@ -37,6 +37,7 @@ type SchedulerConfig struct {
|
||||
QueueSize int // 队列大小
|
||||
RateInterval time.Duration // 速率限制间隔
|
||||
Verbose bool // 是否开启详细日志
|
||||
StrictQueue bool // 是否开启严格排队(满时拒绝执行,不降级直接执行)
|
||||
}
|
||||
|
||||
// TaskType 任务类型
|
||||
@@ -281,9 +282,16 @@ func (s *Scheduler) EnqueueOrExecute(req *ExecutionRequest) {
|
||||
s.handler.OnTaskScheduled(req)
|
||||
}
|
||||
default:
|
||||
// 队列满,直接执行(降级处理)
|
||||
s.logger.Warnf("[Scheduler] 任务队列已满,直接执行任务 %s", req.TaskID)
|
||||
go s.executeTask(req)
|
||||
if s.config.StrictQueue {
|
||||
s.logger.Errorf("[Scheduler] 任务队列已满,拒绝执行任务 %s", req.TaskID)
|
||||
if s.handler != nil {
|
||||
s.handler.OnTaskFailed(req, fmt.Errorf("任务队列已满,拒绝执行"))
|
||||
}
|
||||
} else {
|
||||
// 队列满,直接执行(降级处理)
|
||||
s.logger.Warnf("[Scheduler] 任务队列已满,直接执行任务 %s", req.TaskID)
|
||||
go s.executeTask(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,8 +600,8 @@ func (s *Scheduler) Reload(config SchedulerConfig) {
|
||||
// 重启 workers
|
||||
s.Start()
|
||||
|
||||
s.logger.Infof("[Scheduler] 配置已重载: workers=%d, queue=%d, rate=%v",
|
||||
config.WorkerCount, config.QueueSize, config.RateInterval)
|
||||
s.logger.Infof("[Scheduler] 配置已重载: workers=%d, queue=%d, rate=%v, strict=%t",
|
||||
config.WorkerCount, config.QueueSize, config.RateInterval, config.StrictQueue)
|
||||
}
|
||||
|
||||
// GetQueueSize 获取当前队列大小
|
||||
|
||||
+57
-17
@@ -1,28 +1,68 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
)
|
||||
|
||||
// AgentSchedulerConfig Agent 调度器配置
|
||||
type AgentSchedulerConfig struct {
|
||||
WorkerCount int `json:"worker_count"`
|
||||
QueueSize int `json:"queue_size"`
|
||||
RateInterval time.Duration `json:"rate_interval"`
|
||||
Verbose bool `json:"verbose"`
|
||||
StrictQueue bool `json:"strict_queue"`
|
||||
}
|
||||
|
||||
// Value 序列化为数据库字符串
|
||||
func (c AgentSchedulerConfig) Value() (driver.Value, error) {
|
||||
bytes, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// Scan 反序列化数据库字符串为结构体
|
||||
func (c *AgentSchedulerConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.New("invalid type for AgentSchedulerConfig")
|
||||
}
|
||||
bytes = []byte(str)
|
||||
}
|
||||
return json.Unmarshal(bytes, c)
|
||||
}
|
||||
|
||||
// Agent 远程执行代理
|
||||
type Agent struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||
Name string `json:"name" gorm:"size:100;not null"` // Agent 名称
|
||||
Token string `json:"token" gorm:"size:64;index"` // 认证 Token(可重复使用)
|
||||
MachineID string `json:"machine_id" gorm:"size:64;uniqueIndex"` // 机器识别码(唯一)
|
||||
Description string `json:"description" gorm:"size:255"` // 描述
|
||||
Status string `json:"status" gorm:"size:20;default:'pending';index"` // 状态: constant.AgentStatusOnline, constant.AgentStatusOffline
|
||||
LastSeen *LocalTime `json:"last_seen"` // 最后心跳时间
|
||||
IP string `json:"ip" gorm:"size:45"` // Agent IP 地址
|
||||
Version string `json:"version" gorm:"size:50"` // Agent 版本
|
||||
BuildTime string `json:"build_time" gorm:"size:30"` // Agent 构建时间
|
||||
Hostname string `json:"hostname" gorm:"size:100"` // Agent 主机名
|
||||
OS string `json:"os" gorm:"size:20"` // 操作系统
|
||||
Arch string `json:"arch" gorm:"size:20"` // 架构
|
||||
ForceUpdate bool `json:"force_update" gorm:"default:false"` // 强制更新标志
|
||||
Enabled *bool `json:"enabled" gorm:"default:true"` // 是否启用
|
||||
CreatedAt LocalTime `json:"created_at"`
|
||||
UpdatedAt LocalTime `json:"updated_at"`
|
||||
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||
Name string `json:"name" gorm:"size:100;not null"` // Agent 名称
|
||||
Token string `json:"token" gorm:"size:64;index"` // 认证 Token(可重复使用)
|
||||
MachineID string `json:"machine_id" gorm:"size:64;uniqueIndex"` // 机器识别码(唯一)
|
||||
Description string `json:"description" gorm:"size:255"` // 描述
|
||||
Status string `json:"status" gorm:"size:20;default:'pending';index"` // 状态: constant.AgentStatusOnline, constant.AgentStatusOffline
|
||||
LastSeen *LocalTime `json:"last_seen"` // 最后心跳时间
|
||||
IP string `json:"ip" gorm:"size:45"` // Agent IP 地址
|
||||
Version string `json:"version" gorm:"size:50"` // Agent 版本
|
||||
BuildTime string `json:"build_time" gorm:"size:30"` // Agent 构建时间
|
||||
Hostname string `json:"hostname" gorm:"size:100"` // Agent 主机名
|
||||
OS string `json:"os" gorm:"size:20"` // 操作系统
|
||||
Arch string `json:"arch" gorm:"size:20"` // 架构
|
||||
ForceUpdate bool `json:"force_update" gorm:"default:false"` // 强制更新标志
|
||||
Enabled *bool `json:"enabled" gorm:"default:true"` // 是否启用
|
||||
SchedulerConfig AgentSchedulerConfig `json:"scheduler_config" gorm:"type:text"` // 调度配置,以 JSON 字符串形式存储在 Text 类型字段中
|
||||
CreatedAt LocalTime `json:"created_at"`
|
||||
UpdatedAt LocalTime `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Agent) TableName() string {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/models"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
)
|
||||
@@ -18,10 +20,11 @@ type AgentVO struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt models.LocalTime `json:"created_at"`
|
||||
UpdatedAt models.LocalTime `json:"updated_at"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SchedulerConfig *AgentSchedulerConfigVO `json:"scheduler_config"`
|
||||
CreatedAt models.LocalTime `json:"created_at"`
|
||||
UpdatedAt models.LocalTime `json:"updated_at"`
|
||||
// 隐藏 Token 和 MachineID
|
||||
}
|
||||
|
||||
@@ -30,6 +33,16 @@ func ToAgentVO(agent *models.Agent) *AgentVO {
|
||||
if agent == nil {
|
||||
return nil
|
||||
}
|
||||
var schedulerConfigVO *AgentSchedulerConfigVO
|
||||
if agent.SchedulerConfig.WorkerCount > 0 {
|
||||
schedulerConfigVO = &AgentSchedulerConfigVO{
|
||||
WorkerCount: agent.SchedulerConfig.WorkerCount,
|
||||
QueueSize: agent.SchedulerConfig.QueueSize,
|
||||
RateInterval: int(agent.SchedulerConfig.RateInterval / time.Millisecond),
|
||||
Verbose: agent.SchedulerConfig.Verbose,
|
||||
StrictQueue: agent.SchedulerConfig.StrictQueue,
|
||||
}
|
||||
}
|
||||
return &AgentVO{
|
||||
ID: agent.ID,
|
||||
Name: agent.Name,
|
||||
@@ -42,10 +55,11 @@ func ToAgentVO(agent *models.Agent) *AgentVO {
|
||||
Hostname: agent.Hostname,
|
||||
OS: agent.OS,
|
||||
Arch: agent.Arch,
|
||||
ForceUpdate: agent.ForceUpdate,
|
||||
Enabled: utils.DerefBool(agent.Enabled, true),
|
||||
CreatedAt: agent.CreatedAt,
|
||||
UpdatedAt: agent.UpdatedAt,
|
||||
ForceUpdate: agent.ForceUpdate,
|
||||
Enabled: utils.DerefBool(agent.Enabled, true),
|
||||
SchedulerConfig: schedulerConfigVO,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
UpdatedAt: agent.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,3 +133,12 @@ func ToAgentTokenVOListFromModels(tokens []models.AgentToken) []*AgentTokenVO {
|
||||
}
|
||||
return vos
|
||||
}
|
||||
|
||||
// AgentSchedulerConfigVO 调度配置视图对象
|
||||
type AgentSchedulerConfigVO struct {
|
||||
WorkerCount int `json:"worker_count"`
|
||||
QueueSize int `json:"queue_size"`
|
||||
RateInterval int `json:"rate_interval"` // 毫秒
|
||||
Verbose bool `json:"verbose"`
|
||||
StrictQueue bool `json:"strict_queue"`
|
||||
}
|
||||
|
||||
@@ -203,11 +203,12 @@ func (s *AgentService) Register(req *models.AgentRegisterRequest, ip string) (*m
|
||||
}
|
||||
|
||||
// Update 更新 Agent
|
||||
func (s *AgentService) Update(id string, name, description string, enabled bool) error {
|
||||
func (s *AgentService) Update(id string, name, description string, enabled bool, schedulerConfig models.AgentSchedulerConfig) error {
|
||||
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"name": name,
|
||||
"description": description,
|
||||
"enabled": &enabled,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"enabled": &enabled,
|
||||
"scheduler_config": schedulerConfig,
|
||||
}).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -937,7 +937,17 @@ func (es *ExecutorService) CheckConcurrency(taskID string) error {
|
||||
}
|
||||
|
||||
if config.Concurrency == 0 && len(goids) > 0 {
|
||||
return fmt.Errorf("任务正在运行中,拒绝并行执行,请前往日志查看")
|
||||
// 检查目标 Agent 是否开启了排队机制
|
||||
var isAgentQueueing bool
|
||||
if task.AgentID != nil && *task.AgentID != "" {
|
||||
var agent models.Agent
|
||||
if err := database.DB.Select("scheduler_config").Where("id = ?", *task.AgentID).First(&agent).Error; err == nil {
|
||||
isAgentQueueing = agent.SchedulerConfig.StrictQueue
|
||||
}
|
||||
}
|
||||
if !isAgentQueueing {
|
||||
return fmt.Errorf("任务正在运行中,拒绝并行执行,请前往日志查看")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -969,7 +979,17 @@ func (es *ExecutorService) AddRunningGo(taskID string) (int64, error) {
|
||||
|
||||
// 如果并发为0(禁用)且已有执行中的任务,返回错误
|
||||
if config.Concurrency == 0 && len(goids) > 0 {
|
||||
return fmt.Errorf("task is running")
|
||||
// 检查目标 Agent 是否开启了排队机制
|
||||
var isAgentQueueing bool
|
||||
if task.AgentID != nil && *task.AgentID != "" {
|
||||
var agent models.Agent
|
||||
if err := tx.Select("scheduler_config").Where("id = ?", *task.AgentID).First(&agent).Error; err == nil {
|
||||
isAgentQueueing = agent.SchedulerConfig.StrictQueue
|
||||
}
|
||||
}
|
||||
if !isAgentQueueing {
|
||||
return fmt.Errorf("task is running")
|
||||
}
|
||||
}
|
||||
|
||||
goids = append(goids, goid)
|
||||
|
||||
+10
-1
@@ -279,7 +279,7 @@ export const api = {
|
||||
agents: {
|
||||
list: () => request<Agent[]>('/agents'),
|
||||
getVersion: () => request<{ version: string; platforms: { os: string; arch: string; filename: string }[] }>('/agents/version'),
|
||||
update: (id: string, data: { name: string; description?: string; enabled: boolean }) =>
|
||||
update: (id: string, data: { name: string; description?: string; enabled: boolean; scheduler_config: SchedulerConfig | null }) =>
|
||||
request('/agents/' + id, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id: string) => request('/agents/' + id, { method: 'DELETE' }),
|
||||
forceUpdate: (id: string) => request('/agents/' + id + '/update', { method: 'POST' }),
|
||||
@@ -582,10 +582,19 @@ export interface Agent {
|
||||
os: string
|
||||
arch: string
|
||||
enabled: boolean
|
||||
scheduler_config: SchedulerConfig | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface SchedulerConfig {
|
||||
worker_count: number
|
||||
queue_size: number
|
||||
rate_interval: number
|
||||
verbose: boolean
|
||||
strict_queue: boolean
|
||||
}
|
||||
|
||||
export interface AgentToken {
|
||||
id: string
|
||||
token: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, computed, onUnmounted } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
@@ -38,7 +39,18 @@ const showDownloadDialog = ref(false)
|
||||
const showTokenDialog = ref(false)
|
||||
const showEditTokenDialog = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
const formData = ref({ name: '', description: '' })
|
||||
const customScheduler = ref(false)
|
||||
const formData = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
scheduler_config: {
|
||||
worker_count: 1,
|
||||
queue_size: 100,
|
||||
rate_interval: 200,
|
||||
verbose: false,
|
||||
strict_queue: false
|
||||
}
|
||||
})
|
||||
const tokenForm = ref({ remark: '', max_uses: 0, expires_at: '' })
|
||||
const editingToken = ref<AgentToken | null>(null)
|
||||
const editTokenForm = ref({ remark: '', max_uses: 0, expires_at: '' })
|
||||
@@ -89,14 +101,37 @@ function viewDetail(agent: Agent) {
|
||||
function openEditDialog(agent: Agent) {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
editingAgent.value = agent
|
||||
formData.value = { name: agent.name, description: agent.description }
|
||||
customScheduler.value = !!agent.scheduler_config
|
||||
formData.value = {
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
scheduler_config: agent.scheduler_config ? {
|
||||
worker_count: agent.scheduler_config.worker_count,
|
||||
queue_size: agent.scheduler_config.queue_size,
|
||||
rate_interval: agent.scheduler_config.rate_interval,
|
||||
verbose: agent.scheduler_config.verbose,
|
||||
strict_queue: agent.scheduler_config.strict_queue
|
||||
} : {
|
||||
worker_count: 1,
|
||||
queue_size: 100,
|
||||
rate_interval: 200,
|
||||
verbose: false,
|
||||
strict_queue: false
|
||||
}
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
async function updateAgent() {
|
||||
if (!editingAgent.value || !formData.value.name.trim()) return
|
||||
try {
|
||||
await api.agents.update(editingAgent.value.id, { ...formData.value, enabled: editingAgent.value.enabled })
|
||||
const payload = {
|
||||
name: formData.value.name,
|
||||
description: formData.value.description,
|
||||
enabled: editingAgent.value.enabled,
|
||||
scheduler_config: customScheduler.value ? formData.value.scheduler_config : null
|
||||
}
|
||||
await api.agents.update(editingAgent.value.id, payload)
|
||||
showEditDialog.value = false
|
||||
await loadAgents()
|
||||
toast.success('更新成功')
|
||||
@@ -108,7 +143,12 @@ async function updateAgent() {
|
||||
async function toggleEnabled(agent: Agent) {
|
||||
try {
|
||||
const newEnabled = !agent.enabled
|
||||
await api.agents.update(agent.id, { name: agent.name, description: agent.description, enabled: newEnabled })
|
||||
await api.agents.update(agent.id, {
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
enabled: newEnabled,
|
||||
scheduler_config: agent.scheduler_config
|
||||
})
|
||||
await loadAgents()
|
||||
toast.success(`${agent.name} 已${newEnabled ? '启用' : '禁用'}`)
|
||||
} catch (e: unknown) {
|
||||
@@ -612,6 +652,12 @@ onUnmounted(() => {
|
||||
<Label class="text-muted-foreground text-xs">注册时间</Label>
|
||||
<div class="text-sm">{{ viewingAgent.created_at || '-' }}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between sm:block">
|
||||
<Label class="text-muted-foreground text-xs">任务排队</Label>
|
||||
<div class="text-sm">
|
||||
{{ viewingAgent.scheduler_config ? `自定义 (并发: ${viewingAgent.scheduler_config.worker_count}, 队列: ${viewingAgent.scheduler_config.queue_size}, 严格排队: ${viewingAgent.scheduler_config.strict_queue ? '是' : '否'})` : '继承全局' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="viewingAgent.description" class="pt-2 border-t">
|
||||
<Label class="text-muted-foreground text-xs">描述</Label>
|
||||
@@ -629,13 +675,55 @@ onUnmounted(() => {
|
||||
<DialogDescription class="sr-only">修改 Agent 的名称和描述信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input v-model="formData.name" placeholder="Agent 名称" />
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs font-medium text-foreground">名称</Label>
|
||||
<Input v-model="formData.name" placeholder="Agent 名称" class="h-9" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Input v-model="formData.description" placeholder="描述信息(可选)" />
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs font-medium text-foreground">描述</Label>
|
||||
<Input v-model="formData.description" placeholder="描述信息(可选)" class="h-9" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div class="space-y-0.5">
|
||||
<Label class="text-sm font-medium">自定义调度配置</Label>
|
||||
<div class="text-xs text-muted-foreground">开启后可独立配置该 Agent 的并发限制与任务排队参数</div>
|
||||
</div>
|
||||
<Switch v-model="customScheduler" />
|
||||
</div>
|
||||
<div v-if="customScheduler" class="p-4 rounded-lg border border-border bg-muted/20 space-y-4 mt-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs font-medium text-foreground">并发限制数 (Workers)</Label>
|
||||
<Input type="number" v-model.number="formData.scheduler_config.worker_count" :min="1" class="h-9" />
|
||||
<p class="text-[10px] text-muted-foreground">同一时间最大并行任务数</p>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs font-medium text-foreground">最大队列数 (Queue Size)</Label>
|
||||
<Input type="number" v-model.number="formData.scheduler_config.queue_size" :min="1" class="h-9" />
|
||||
<p class="text-[10px] text-muted-foreground">并发满时等待排队的任务数</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs font-medium text-foreground">调度频率限制 (Rate Interval)</Label>
|
||||
<div class="relative">
|
||||
<Input type="number" v-model.number="formData.scheduler_config.rate_interval" :min="0" class="h-9 pr-10" />
|
||||
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">ms</span>
|
||||
</div>
|
||||
<p class="text-[10px] text-muted-foreground">两次调度启动的最小间隔时间</p>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs font-medium text-foreground">执行降级策略</Label>
|
||||
<div class="flex items-center justify-between rounded-md border border-input bg-card px-3 h-9 shadow-sm">
|
||||
<span class="text-xs text-muted-foreground">队列满时拒绝执行</span>
|
||||
<Switch v-model="formData.scheduler_config.strict_queue" class="scale-90" />
|
||||
</div>
|
||||
<p class="text-[10px] text-muted-foreground">开启后拒绝执行;关闭则同步直接执行</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md bg-yellow-500/10 border border-yellow-500/20 p-2.5 text-[10px] text-yellow-600 dark:text-yellow-400 leading-relaxed">
|
||||
<strong>提示:</strong>开启严格排队后,服务端将不再拦截此 Agent 上已运行任务的并行触发,而是交由 Agent 本地队列调度。若要严格保证任务不并行,请将 <strong>并发限制数 (Workers)</strong> 设为 1。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user