From 685d79b54e169cec2c100001f3c3df036244f509 Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Wed, 29 Apr 2026 18:02:43 +0800 Subject: [PATCH] feat: add task pin #89 --- internal/constant/constant.go | 4 + internal/controllers/task_controller.go | 8 +- internal/models/task.go | 1 + internal/models/vo/task_vo.go | 2 + internal/services/tasks/task_service.go | 13 +- internal/utils/network.go | 7 +- web/src/api/index.ts | 1 + web/src/views/tasks/RepoDialog.vue | 1 + web/src/views/tasks/TaskDialog.vue | 1 + web/src/views/tasks/Tasks.vue | 213 ++++++++++++++++++------ 10 files changed, 186 insertions(+), 65 deletions(-) diff --git a/internal/constant/constant.go b/internal/constant/constant.go index dc76c65..74855ed 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -133,6 +133,10 @@ const ( TaskTypeNormal = "task" TaskTypeRepo = "repo" + // 任务置顶类型 + PinTypeNone = "none" + PinTypeTop = "top" + // 触发类型 TriggerTypeCron = "cron" TriggerTypeBaihuStartup = "baihu_startup" diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 008909a..cba712e 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -73,6 +73,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) { RetryCount int `json:"retry_count"` RetryInterval int `json:"retry_interval"` RandomRange int `json:"random_range"` + PinType string `json:"pin_type"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -116,12 +117,12 @@ func (tc *TaskController) CreateTask(c *gin.Context) { if sourceID != "" { task = tc.taskService.GetTaskBySourceID(sourceID) if task != nil { - task = tc.taskService.UpdateTask(task.ID, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, true, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID) + task = tc.taskService.UpdateTask(task.ID, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, true, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType) } } if task == nil { - 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, req.RandomRange, sourceID) + 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, req.RandomRange, sourceID, req.PinType) } // 如果是 Agent 任务,通知 Agent;否则添加到本地 cron @@ -237,6 +238,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { RetryCount int `json:"retry_count"` RetryInterval int `json:"retry_interval"` RandomRange int `json:"random_range"` + PinType string `json:"pin_type"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -270,7 +272,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { sourceID = oldTask.SourceID } - 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, req.RandomRange, sourceID) + 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, req.RandomRange, sourceID, req.PinType) if task == nil { utils.NotFound(c, "任务不存在") return diff --git a/internal/models/task.go b/internal/models/task.go index e1379c7..5e0f566 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -71,6 +71,7 @@ type Task struct { ID string `json:"id" gorm:"primaryKey;size:20"` Name string `json:"name" gorm:"size:255;not null"` Remark string `json:"remark" gorm:"size:255;default:''"` + PinType string `json:"pin_type" gorm:"size:20;default:none;index"` // 置顶类型: constant.PinTypeNone, constant.PinTypeTop Command BigText `json:"command"` // 普通任务的命令 Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔 Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo diff --git a/internal/models/vo/task_vo.go b/internal/models/vo/task_vo.go index 1661fb7..a5d86c7 100644 --- a/internal/models/vo/task_vo.go +++ b/internal/models/vo/task_vo.go @@ -27,6 +27,7 @@ type TaskVO struct { RetryCount int `json:"retry_count"` RetryInterval int `json:"retry_interval"` RandomRange int `json:"random_range"` + PinType string `json:"pin_type"` LastRun *models.LocalTime `json:"last_run"` NextRun *models.LocalTime `json:"next_run"` CreatedAt models.LocalTime `json:"created_at"` @@ -58,6 +59,7 @@ func ToTaskVO(task *models.Task) *TaskVO { RetryCount: task.RetryCount, RetryInterval: task.RetryInterval, RandomRange: task.RandomRange, + PinType: task.PinType, LastRun: task.LastRun, NextRun: task.NextRun, CreatedAt: task.CreatedAt, diff --git a/internal/services/tasks/task_service.go b/internal/services/tasks/task_service.go index 1fe32c5..b97a84e 100644 --- a/internal/services/tasks/task_service.go +++ b/internal/services/tasks/task_service.go @@ -24,17 +24,21 @@ func (ts *TaskService) GetTaskBySourceID(sourceID string) *models.Task { return &task } -func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string) *models.Task { +func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task { if taskType == "" { taskType = "task" } if triggerType == "" { triggerType = constant.TriggerTypeCron } + if pinType == "" { + pinType = constant.PinTypeNone + } task := &models.Task{ ID: utils.GenerateID(), Name: name, Command: models.BigText(command), + PinType: pinType, Tags: tags, Type: taskType, TriggerType: triggerType, @@ -104,7 +108,7 @@ func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, a } query.Count(&total) - query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks) + query.Order("pin_type DESC, updated_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks) return tasks, total } @@ -118,7 +122,7 @@ func (ts *TaskService) GetTaskByID(id string) *models.Task { return &task } -func (ts *TaskService) UpdateTask(id string, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string) *models.Task { +func (ts *TaskService) UpdateTask(id string, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task { var task models.Task res := database.DB.Where("id = ?", id).Limit(1).Find(&task) if res.Error != nil || res.RowsAffected == 0 { @@ -126,6 +130,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim } task.Name = name task.Command = models.BigText(command) + task.PinType = pinType task.Tags = tags task.Schedule = schedule task.Timeout = timeout @@ -153,7 +158,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim "Name", "Command", "Tags", "Schedule", "Timeout", "WorkDir", "CleanConfig", "Envs", "Enabled", "AgentID", "Languages", "RetryCount", "RetryInterval", "RandomRange", "Type", - "TriggerType", "Config", "SourceID", + "TriggerType", "Config", "SourceID", "PinType", ).Updates(&task) return &task diff --git a/internal/utils/network.go b/internal/utils/network.go index 9a1aa7f..193e9ea 100644 --- a/internal/utils/network.go +++ b/internal/utils/network.go @@ -49,9 +49,10 @@ func CheckWSOrigin(r *http.Request) bool { } } - // 3. 兜底策略:如果是开发环境常见的 localhost/127.0.0.1,且端口不一致的情况, - // 如果用户没有配置允许列表,我们在非 Release 模式下可以考虑放行, - // 但为了安全,默认应严格限制。建议开发时通过 BH_ALLOWED_ORIGINS=localhost:5173 显式开启。 + // 3. 允许来自 localhost 和 127.0.0.1 的请求 (方便本地开发和同机部署) + if strings.HasPrefix(u.Host, "localhost") || strings.HasPrefix(u.Host, "127.0.0.1") { + return true + } return false } diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 7bea2f9..a951584 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -356,6 +356,7 @@ export interface Task { retry_count: number retry_interval: number random_range: number + pin_type: 'none' | 'top' languages: { name: string; version: string }[] agent_id: string | null enabled: boolean diff --git a/web/src/views/tasks/RepoDialog.vue b/web/src/views/tasks/RepoDialog.vue index e158625..d41c27f 100644 --- a/web/src/views/tasks/RepoDialog.vue +++ b/web/src/views/tasks/RepoDialog.vue @@ -291,6 +291,7 @@ watch(() => props.open, async (val: boolean) => { retry_interval: props.task?.retry_interval ?? 0, random_range: props.task?.random_range ?? 0, timeout: props.task?.timeout ?? 30, + pin_type: props.task?.pin_type ?? 'none', ...props.task } // 解析清理配置 diff --git a/web/src/views/tasks/TaskDialog.vue b/web/src/views/tasks/TaskDialog.vue index e72effd..015f13b 100644 --- a/web/src/views/tasks/TaskDialog.vue +++ b/web/src/views/tasks/TaskDialog.vue @@ -239,6 +239,7 @@ watch(() => props.open, async (val: boolean) => { retry_interval: props.task?.retry_interval ?? 0, random_range: props.task?.random_range ?? 0, timeout: props.task?.timeout ?? 30, + pin_type: props.task?.pin_type ?? 'none', ...props.task } // 解析清理配置 diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index 9656790..d95b161 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -7,11 +7,18 @@ import Pagination from '@/components/Pagination.vue' import TaskDialog from './TaskDialog.vue' import RepoDialog from './RepoDialog.vue' import LogViewer from '@/views/history/LogViewer.vue' -import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag, ChevronDown } from 'lucide-vue-next' +import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag, ChevronDown, Pin, PinOff, MoreHorizontal } from 'lucide-vue-next' import TagInput from '@/components/TagInput.vue' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from '@/components/ui/dropdown-menu' import { api, type Agent, type Task, type TaskLog } from '@/api' import { toast } from 'vue-sonner' import { useSiteSettings } from '@/composables/useSiteSettings' @@ -223,6 +230,15 @@ async function toggleTask(task: Task, enabled: boolean) { } catch { toast.error('操作失败') } } +async function togglePin(task: Task) { + const newType = task.pin_type === 'top' ? 'none' : 'top' + try { + await api.tasks.update(task.id, { ...task, pin_type: newType }) + toast.success(newType === 'top' ? '任务已置顶' : '已取消置顶') + loadTasks() + } catch { toast.error('置顶操作失败') } +} + const showLogViewer = ref(false) const selectedLog = ref(null) const logContent = ref('') @@ -414,7 +430,7 @@ watch(() => route.query.agent_id, (newVal: any) => {