From f096d6dea4cbd3a1af4ecc5f85b93e948cd90d65 Mon Sep 17 00:00:00 2001 From: engigu Date: Thu, 25 Dec 2025 17:55:54 +0800 Subject: [PATCH] feat: add repo sync page --- Dockerfile | 7 +- internal/controllers/task_controller.go | 16 +- internal/models/task.go | 17 +- internal/services/executor_service.go | 101 ++++++++- internal/services/task_service.go | 13 +- sync.py | 265 ++++++++++++++++++++++++ web/src/api/index.ts | 14 ++ web/src/assets/index.css | 38 ++++ web/src/views/tasks/Tasks.vue | 206 +++++++++++++++++- 9 files changed, 655 insertions(+), 22 deletions(-) create mode 100644 sync.py diff --git a/Dockerfile b/Dockerfile index e0c020f..f720c79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,7 +74,12 @@ COPY --from=frontend-builder /app/web/dist ./internal/static/dist # Copy configs and entrypoint COPY --from=backend-builder /app/configs ./configs COPY docker-entrypoint.sh . -RUN chmod +x docker-entrypoint.sh && touch "dont-not-delete-anythings|不要删除这里的任何东西" + +# Copy sync.py to /opt +COPY sync.py /opt/sync.py +RUN chmod +x /opt/sync.py \ + && chmod +x docker-entrypoint.sh \ + && touch "dont-not-delete-anythings" EXPOSE 8052 diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 8b196cc..366d0a6 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -49,7 +49,9 @@ func resolveWorkDir(workDir string) string { func (tc *TaskController) CreateTask(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` - Command string `json:"command" binding:"required"` + Command string `json:"command"` + Type string `json:"type"` + Config string `json:"config"` Schedule string `json:"schedule" binding:"required"` Timeout int `json:"timeout"` WorkDir string `json:"work_dir"` @@ -62,6 +64,12 @@ func (tc *TaskController) CreateTask(c *gin.Context) { return } + // 普通任务需要命令 + if req.Type != "repo" && req.Command == "" { + utils.BadRequest(c, "命令不能为空") + return + } + if err := tc.cronService.ValidateCron(req.Schedule); err != nil { utils.BadRequest(c, "无效的cron表达式: "+err.Error()) return @@ -70,7 +78,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) + task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config) tc.cronService.AddTask(task) utils.Success(c, task) @@ -110,6 +118,8 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { var req struct { Name string `json:"name"` Command string `json:"command"` + Type string `json:"type"` + Config string `json:"config"` Schedule string `json:"schedule"` Timeout int `json:"timeout"` WorkDir string `json:"work_dir"` @@ -130,7 +140,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { } } - task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, resolveWorkDir(req.WorkDir), req.CleanConfig, req.Envs, req.Enabled) + task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, resolveWorkDir(req.WorkDir), req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config) if task == nil { utils.NotFound(c, "任务不存在") return diff --git a/internal/models/task.go b/internal/models/task.go index a751c53..5485493 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -12,11 +12,26 @@ type CleanConfig struct { Keep int `json:"keep"` // 保留天数或条数 } +// RepoConfig 仓库同步配置 +type RepoConfig struct { + SourceType string `json:"source_type"` // url 或 git + SourceURL string `json:"source_url"` // 源地址 + TargetPath string `json:"target_path"` // 目标路径 + Branch string `json:"branch"` // Git 分支 + SparsePath string `json:"sparse_path"` // 稀疏检出路径(仅拉取指定目录或文件) + SingleFile bool `json:"single_file"` // 单文件模式(直接下载文件而非 sparse-checkout) + Proxy string `json:"proxy"` // 代理类型: none, ghproxy, mirror, custom + ProxyURL string `json:"proxy_url"` // 自定义代理地址 + AuthToken string `json:"auth_token"` // 认证 Token +} + // Task represents a scheduled task type Task struct { ID uint `json:"id" gorm:"primaryKey"` Name string `json:"name" gorm:"size:255;not null"` - Command string `json:"command" gorm:"type:text;not null"` + Command string `json:"command" gorm:"type:text"` // 普通任务的命令 + Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: task(普通任务), repo(仓库同步) + Config string `json:"config" gorm:"type:text"` // 配置 JSON(仓库同步配置等) Schedule string `json:"schedule" gorm:"size:100"` // cron expression Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟 WorkDir string `json:"work_dir" gorm:"size:255;default:''"` // 工作目录,为空则使用 scripts 目录 diff --git a/internal/services/executor_service.go b/internal/services/executor_service.go index 4f097f8..f7f8318 100644 --- a/internal/services/executor_service.go +++ b/internal/services/executor_service.go @@ -12,6 +12,7 @@ import ( "fmt" "os" "os/exec" + "strings" "sync" "time" ) @@ -296,6 +297,30 @@ func (es *ExecutorService) executeTaskInternal(taskID int) *ExecutionResult { es.runningTasks[taskID] = true es.mu.Unlock() + var result *ExecutionResult + + // 根据任务类型执行不同逻辑 + if task.Type == "repo" { + result = es.executeRepoTask(task) + } else { + result = es.executeNormalTask(task) + } + + result.TaskID = taskID + + // 标记任务结束 + es.mu.Lock() + delete(es.runningTasks, taskID) + es.mu.Unlock() + + // 异步执行回调(日志压缩、统计更新、日志清理) + es.executeCallbacksAsync(uint(taskID), task.Command, result) + + return result +} + +// executeNormalTask 执行普通任务 +func (es *ExecutorService) executeNormalTask(task *models.Task) *ExecutionResult { // 加载环境变量 envService := NewEnvService() envVars := envService.GetEnvVarsByIDs(task.Envs) @@ -311,16 +336,76 @@ func (es *ExecutorService) executeTaskInternal(taskID int) *ExecutionResult { if timeout <= 0 { timeout = constant.DefaultTaskTimeout } - result := es.ExecuteCommandWithOptions(task.Command, time.Duration(timeout)*time.Minute, envVars, workDir) - result.TaskID = taskID + return es.ExecuteCommandWithOptions(task.Command, time.Duration(timeout)*time.Minute, envVars, workDir) +} - // 标记任务结束 - es.mu.Lock() - delete(es.runningTasks, taskID) - es.mu.Unlock() +// executeRepoTask 执行仓库同步任务(调用 sync.py) +func (es *ExecutorService) executeRepoTask(task *models.Task) *ExecutionResult { + result := &ExecutionResult{ + Success: false, + Start: time.Now(), + } - // 异步执行回调(日志压缩、统计更新、日志清理) - es.executeCallbacksAsync(uint(taskID), task.Command, result) + // 解析仓库配置 + var config models.RepoConfig + if err := json.Unmarshal([]byte(task.Config), &config); err != nil { + result.End = time.Now() + result.Error = "解析仓库配置失败: " + err.Error() + return result + } + + // 构建 sync.py 命令参数 + args := []string{ + "/opt/sync.py", + "--source-type", config.SourceType, + "--source-url", config.SourceURL, + "--target-path", config.TargetPath, + } + + // Git 分支 + if config.Branch != "" { + args = append(args, "--branch", config.Branch) + } + + // 稀疏路径 + if config.SparsePath != "" { + args = append(args, "--path", config.SparsePath) + } + + // 单文件模式 + if config.SingleFile { + args = append(args, "--single-file") + } + + // 代理设置 + if config.Proxy != "" && config.Proxy != "none" { + args = append(args, "--proxy", config.Proxy) + if config.Proxy == "custom" && config.ProxyURL != "" { + args = append(args, "--proxy-url", config.ProxyURL) + } + } + + // 认证 Token + if config.AuthToken != "" { + args = append(args, "--auth-token", config.AuthToken) + } + + // 构建命令 + command := "python3 " + strings.Join(args, " ") + + // 使用任务配置的超时时间 + timeout := task.Timeout + if timeout <= 0 { + timeout = constant.DefaultTaskTimeout + } + + // 执行命令 + execResult := es.ExecuteCommandWithOptions(command, time.Duration(timeout)*time.Minute, nil, "/opt") + + result.End = time.Now() + result.Output = execResult.Output + result.Success = execResult.Success + result.Error = execResult.Error return result } diff --git a/internal/services/task_service.go b/internal/services/task_service.go index fc1b798..b9aea18 100644 --- a/internal/services/task_service.go +++ b/internal/services/task_service.go @@ -11,10 +11,15 @@ func NewTaskService() *TaskService { return &TaskService{} } -func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs string) *models.Task { +func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string) *models.Task { + if taskType == "" { + taskType = "task" + } task := &models.Task{ Name: name, Command: command, + Type: taskType, + Config: config, Schedule: schedule, Timeout: timeout, WorkDir: workDir, @@ -56,7 +61,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) *models.Task { +func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string) *models.Task { var task models.Task if err := database.DB.First(&task, id).Error; err != nil { return nil @@ -69,6 +74,10 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeou task.CleanConfig = cleanConfig task.Envs = envs task.Enabled = enabled + if taskType != "" { + task.Type = taskType + } + task.Config = config database.DB.Save(&task) return &task } diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..c264c2c --- /dev/null +++ b/sync.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import os +import subprocess +import sys +import shutil +import urllib.request +import urllib.error + + +def run(cmd, env=None, cwd=None): + """执行命令并打印输出""" + print(">>", " ".join(cmd)) + result = subprocess.run( + cmd, + cwd=cwd, + env=env, + stdout=sys.stdout, + stderr=sys.stderr, + ) + if result.returncode != 0: + sys.exit(result.returncode) + + +def build_proxy_url(url, proxy_type, proxy_url): + """构建代理 URL""" + if not proxy_type or proxy_type == "none": + return url + + proxy_base = "" + if proxy_type == "ghproxy": + proxy_base = "https://gh-proxy.com/" + elif proxy_type == "mirror": + proxy_base = "https://mirror.ghproxy.com/" + elif proxy_type == "custom" and proxy_url: + proxy_base = proxy_url.rstrip("/") + "/" + + if proxy_base and url.startswith("http"): + return proxy_base + url + + return url + + +def sync_git_file(args, repo_url, env): + """从 Git 仓库同步单个文件(通过 raw URL 下载)""" + source_url = args.source_url + file_path = args.path + branch = args.branch or "main" + dest = args.target_path + + # 构建 raw 文件 URL + # GitHub: https://github.com/user/repo -> https://raw.githubusercontent.com/user/repo/branch/path + # GitLab: https://gitlab.com/user/repo -> https://gitlab.com/user/repo/-/raw/branch/path + # Gitee: https://gitee.com/user/repo -> https://gitee.com/user/repo/raw/branch/path + + raw_url = None + if "github.com" in source_url: + # GitHub + base = source_url.replace("github.com", "raw.githubusercontent.com").rstrip(".git") + raw_url = f"{base}/{branch}/{file_path}" + elif "gitlab.com" in source_url: + # GitLab + base = source_url.rstrip(".git") + raw_url = f"{base}/-/raw/{branch}/{file_path}" + elif "gitee.com" in source_url: + # Gitee + base = source_url.rstrip(".git") + raw_url = f"{base}/raw/{branch}/{file_path}" + else: + # 通用:尝试 GitHub 风格 + base = source_url.rstrip(".git") + raw_url = f"{base}/raw/{branch}/{file_path}" + + # 应用代理 + raw_url = build_proxy_url(raw_url, args.proxy, args.proxy_url) + + print(f"下载单文件: {raw_url}") + print(f"目标路径: {dest}") + + # 确保目标目录存在 + parent_dir = os.path.dirname(dest) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) + + # 创建请求 + req = urllib.request.Request(raw_url) + + # 添加认证 Token + if args.auth_token: + req.add_header("Authorization", f"token {args.auth_token}") + + req.add_header("User-Agent", "Mozilla/5.0 (compatible; sync.py)") + + try: + with urllib.request.urlopen(req, timeout=300) as response: + content = response.read() + + with open(dest, "wb") as f: + f.write(content) + + print(f"文件大小: {len(content)} 字节") + print("同步完成") + except urllib.error.HTTPError as e: + print(f"下载失败, HTTP 状态码: {e.code}") + sys.exit(1) + except urllib.error.URLError as e: + print(f"下载失败: {e.reason}") + sys.exit(1) + + +def sync_git(args): + """Git 仓库同步""" + env = os.environ.copy() + + # 设置 HTTP 代理 + if args.http_proxy: + env["http_proxy"] = args.http_proxy + env["https_proxy"] = args.http_proxy + + # 构建仓库 URL(带代理) + repo_url = build_proxy_url(args.source_url, args.proxy, args.proxy_url) + + # 如果有认证 Token,将其嵌入 URL + if args.auth_token and repo_url.startswith("https://"): + repo_url = repo_url.replace("https://", f"https://{args.auth_token}@") + + dest = args.target_path + branch = args.branch or "main" + + # 如果指定了 path 且是单文件模式,使用 raw URL 下载 + if args.path and args.single_file: + sync_git_file(args, repo_url, env) + return + + # 检查目标目录是否已存在 git 仓库 + git_dir = os.path.join(dest, ".git") + is_existing_repo = os.path.exists(git_dir) + + if is_existing_repo: + # 已存在仓库,执行 git pull + print(f"检测到已存在仓库,执行 git pull") + + # 先切换分支 + if branch: + try: + run(["git", "checkout", branch], cwd=dest, env=env) + except: + pass + + run(["git", "pull"], cwd=dest, env=env) + else: + # 新仓库,执行 git clone + print(f"执行 git clone") + + # 确保父目录存在 + parent_dir = os.path.dirname(dest) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) + + # 稀疏 clone(如果指定了 path) + if args.path: + run([ + "git", "clone", + "--depth", "1", + "--filter=blob:none", + "--no-checkout", + "-b", branch, + repo_url, + dest + ], env=env) + + run(["git", "sparse-checkout", "init", "--cone"], cwd=dest, env=env) + run(["git", "sparse-checkout", "set", args.path], cwd=dest, env=env) + run(["git", "checkout"], cwd=dest, env=env) + else: + # 普通 clone + run([ + "git", "clone", + "--depth", "1", + "-b", branch, + repo_url, + dest + ], env=env) + + print("同步完成") + + +def sync_url(args): + """URL 文件下载""" + # 构建下载 URL(带代理) + download_url = build_proxy_url(args.source_url, args.proxy, args.proxy_url) + + print(f"下载地址: {download_url}") + + dest = args.target_path + + # 确保目标目录存在 + parent_dir = os.path.dirname(dest) + if parent_dir: + os.makedirs(parent_dir, exist_ok=True) + + # 创建请求 + req = urllib.request.Request(download_url) + + # 添加认证 Token + if args.auth_token: + req.add_header("Authorization", f"token {args.auth_token}") + + # 添加 User-Agent + req.add_header("User-Agent", "Mozilla/5.0 (compatible; sync.py)") + + try: + with urllib.request.urlopen(req, timeout=300) as response: + content = response.read() + + with open(dest, "wb") as f: + f.write(content) + + print(f"目标路径: {dest}") + print(f"文件大小: {len(content)} 字节") + print("同步完成") + except urllib.error.HTTPError as e: + print(f"下载失败, HTTP 状态码: {e.code}") + sys.exit(1) + except urllib.error.URLError as e: + print(f"下载失败: {e.reason}") + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser(description="仓库/文件同步工具") + + parser.add_argument("--source-type", choices=["git", "url"], default="git", + help="源类型: git(Git仓库) 或 url(URL下载)") + parser.add_argument("--source-url", required=True, + help="源地址(Git仓库URL或文件URL)") + parser.add_argument("--target-path", required=True, + help="目标路径") + parser.add_argument("--branch", default="main", + help="Git 分支名(仅 git 类型有效)") + parser.add_argument("--path", + help="仅拉取指定文件或目录(仅 git 类型有效)") + parser.add_argument("--single-file", action="store_true", + help="单文件模式,直接下载指定文件而非 sparse-checkout(需配合 --path 使用)") + parser.add_argument("--proxy", choices=["none", "ghproxy", "mirror", "custom"], default="none", + help="代理类型") + parser.add_argument("--proxy-url", + help="自定义代理地址(仅 proxy=custom 时有效)") + parser.add_argument("--auth-token", + help="认证 Token(用于私有仓库)") + parser.add_argument("--http-proxy", + help="HTTP 代理(如 http://127.0.0.1:7890)") + + args = parser.parse_args() + + if args.source_type == "git": + sync_git(args) + else: + sync_url(args) + + +if __name__ == "__main__": + main() diff --git a/web/src/api/index.ts b/web/src/api/index.ts index a9be77e..c9ff6d4 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -220,6 +220,8 @@ export interface Task { id: number name: string command: string + type: string + config: string schedule: string timeout: number work_dir: string @@ -230,6 +232,18 @@ export interface Task { next_run: string } +export interface RepoConfig { + source_type: string + source_url: string + target_path: string + branch: string + sparse_path: string + single_file: boolean + proxy: string + proxy_url: string + auth_token: string +} + export interface TaskListResponse { data: Task[] total: number diff --git a/web/src/assets/index.css b/web/src/assets/index.css index cc3670f..51a7e93 100644 --- a/web/src/assets/index.css +++ b/web/src/assets/index.css @@ -164,3 +164,41 @@ input[type="number"]:hover::-webkit-outer-spin-button { .dark input[type="number"]:hover::-webkit-outer-spin-button { filter: invert(0.85); } + +/* Custom scrollbar styles */ +.custom-scrollbar::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; + border-radius: 3px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: oklch(0.7 0 0 / 30%); + border-radius: 3px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0 0 / 50%); +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb { + background: oklch(0.5 0 0 / 40%); +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0 0 / 60%); +} + +/* Firefox scrollbar */ +.custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: oklch(0.7 0 0 / 30%) transparent; +} + +.dark .custom-scrollbar { + scrollbar-color: oklch(0.5 0 0 / 40%) transparent; +} diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index 231c6d6..ac32dd5 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -8,10 +8,11 @@ import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Badge } from '@/components/ui/badge' +import { Checkbox } from '@/components/ui/checkbox' import Pagination from '@/components/Pagination.vue' import DirTreeSelect from '@/components/DirTreeSelect.vue' -import { Plus, Play, Pencil, Trash2, Search, ScrollText, ChevronDown, X } from 'lucide-vue-next' -import { api, type Task, type EnvVar } from '@/api' +import { Plus, Play, Pencil, Trash2, Search, ScrollText, ChevronDown, X, GitBranch } from 'lucide-vue-next' +import { api, type Task, type EnvVar, type RepoConfig } from '@/api' import { toast } from 'vue-sonner' import { useSiteSettings } from '@/composables/useSiteSettings' import { useRouter } from 'vue-router' @@ -22,6 +23,7 @@ const { pageSize } = useSiteSettings() const tasks = ref([]) const showDialog = ref(false) +const showRepoDialog = ref(false) const editingTask = ref>({}) const isEdit = ref(false) const showDeleteDialog = ref(false) @@ -31,6 +33,19 @@ const deleteTaskId = ref(null) const cleanType = ref('') const cleanKeep = ref(30) +// 仓库同步配置 +const repoConfig = ref({ + source_type: 'git', + source_url: '', + target_path: '', + branch: '', + sparse_path: '', + single_file: false, + proxy: 'none', + proxy_url: '', + auth_token: '' +}) + // 环境变量 const allEnvVars = ref([]) const selectedEnvIds = ref([]) @@ -53,6 +68,13 @@ const cronPresets = [ { label: '每月1号', value: '0 0 0 1 * *' }, ] +const proxyOptions = [ + { label: '不使用代理', value: 'none' }, + { label: 'ghproxy.com', value: 'ghproxy' }, + { label: 'mirror.ghproxy.com', value: 'mirror' }, + { label: '自定义代理', value: 'custom' }, +] + // 计算清理配置 JSON const cleanConfig = computed(() => { if (!cleanType.value || cleanType.value === 'none' || cleanKeep.value <= 0) return '' @@ -120,7 +142,7 @@ function handlePageChange(page: number) { } function openCreate() { - editingTask.value = { name: '', command: '', schedule: '0 * * * * *', timeout: 30, work_dir: '', enabled: true, clean_config: '', envs: '' } + editingTask.value = { name: '', command: '', type: 'task', schedule: '0 * * * * *', timeout: 30, work_dir: '', enabled: true, clean_config: '', envs: '' } cleanType.value = 'none' cleanKeep.value = 30 selectedEnvIds.value = [] @@ -129,6 +151,15 @@ function openCreate() { showDialog.value = true } +function openCreateRepo() { + editingTask.value = { name: '', type: 'repo', schedule: '0 0 0 * * *', timeout: 30, enabled: true, clean_config: '', envs: '' } + repoConfig.value = { source_type: 'git', source_url: '', target_path: '', branch: '', sparse_path: '', single_file: false, proxy: 'none', proxy_url: '', auth_token: '' } + cleanType.value = 'none' + cleanKeep.value = 30 + isEdit.value = false + showRepoDialog.value = true +} + function openEdit(task: Task) { editingTask.value = { ...task } // 解析清理配置 @@ -153,13 +184,27 @@ function openEdit(task: Task) { } envSearchQuery.value = '' isEdit.value = true - showDialog.value = true + + // 根据任务类型打开不同弹窗 + if (task.type === 'repo') { + if (task.config) { + try { + repoConfig.value = JSON.parse(task.config) + } catch { + repoConfig.value = { source_type: 'git', source_url: '', target_path: '', branch: '', sparse_path: '', single_file: false, proxy: 'none', proxy_url: '', auth_token: '' } + } + } + showRepoDialog.value = true + } else { + showDialog.value = true + } } async function saveTask() { try { editingTask.value.clean_config = cleanConfig.value editingTask.value.envs = envsString.value + editingTask.value.type = 'task' if (isEdit.value && editingTask.value.id) { await api.tasks.update(editingTask.value.id, editingTask.value) toast.success('任务已更新') @@ -172,6 +217,24 @@ async function saveTask() { } catch { toast.error('保存失败') } } +async function saveRepoTask() { + try { + editingTask.value.clean_config = cleanConfig.value + editingTask.value.type = 'repo' + editingTask.value.config = JSON.stringify(repoConfig.value) + editingTask.value.command = `[${repoConfig.value.source_type}] ${repoConfig.value.source_url}` + if (isEdit.value && editingTask.value.id) { + await api.tasks.update(editingTask.value.id, editingTask.value) + toast.success('同步任务已更新') + } else { + await api.tasks.create(editingTask.value) + toast.success('同步任务已创建') + } + showRepoDialog.value = false + loadTasks() + } catch { toast.error('保存失败') } +} + function confirmDelete(id: number) { deleteTaskId.value = id showDeleteDialog.value = true @@ -194,7 +257,7 @@ async function runTask(id: number) { async function toggleTask(task: Task, enabled: boolean) { try { - await api.tasks.update(task.id, { name: task.name, command: task.command, schedule: task.schedule, timeout: task.timeout, work_dir: task.work_dir, clean_config: task.clean_config, envs: task.envs, enabled }) + await api.tasks.update(task.id, { ...task, enabled }) toast.success(enabled ? '任务已启用' : '任务已禁用') loadTasks() } catch { toast.error('操作失败') } @@ -204,6 +267,10 @@ function viewLogs(taskId: number) { router.push({ path: '/history', query: { task_id: String(taskId) } }) } +function getTaskTypeLabel(type: string) { + return type === 'repo' ? '仓库' : '普通' +} + onMounted(() => { loadTasks() loadEnvVars() @@ -222,6 +289,9 @@ onMounted(() => { + @@ -232,8 +302,9 @@ onMounted(() => {
ID + 类型 名称 - 命令 + 命令/地址 @@ -251,11 +322,16 @@ onMounted(() => { class="flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors" > #{{ task.id }} + + + {{ getTaskTypeLabel(task.type || 'task') }} + + - + @@ -283,6 +359,7 @@ onMounted(() => {
+ @@ -392,6 +469,121 @@ onMounted(() => { + + + + + {{ isEdit ? '编辑仓库同步' : '新建仓库同步' }} + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+ + 直接下载文件(适用于单个文件同步) +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

格式: 秒 分 时 日 月 周

+
+ + {{ preset.label }} + +
+
+
+
+ + +
+
+ +
+ + +
+
+
+ + + + +
+
+