fix: agent workdir error
This commit is contained in:
@@ -96,8 +96,17 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
|||||||
func (tc *TaskController) GetTasks(c *gin.Context) {
|
func (tc *TaskController) GetTasks(c *gin.Context) {
|
||||||
p := utils.ParsePagination(c)
|
p := utils.ParsePagination(c)
|
||||||
name := c.DefaultQuery("name", "")
|
name := c.DefaultQuery("name", "")
|
||||||
|
agentIDStr := c.DefaultQuery("agent_id", "")
|
||||||
|
|
||||||
|
var agentID *uint
|
||||||
|
if agentIDStr != "" {
|
||||||
|
if id, err := strconv.ParseUint(agentIDStr, 10, 32); err == nil {
|
||||||
|
uid := uint(id)
|
||||||
|
agentID = &uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name)
|
tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name, agentID)
|
||||||
utils.PaginatedResponse(c, tasks, total, p)
|
utils.PaginatedResponse(c, tasks, total, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (ts *TaskService) GetTasks() []models.Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTasksWithPagination 分页获取任务列表
|
// GetTasksWithPagination 分页获取任务列表
|
||||||
func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string) ([]models.Task, int64) {
|
func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, agentID *uint) ([]models.Task, int64) {
|
||||||
var tasks []models.Task
|
var tasks []models.Task
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
@@ -47,6 +47,9 @@ func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string) (
|
|||||||
if name != "" {
|
if name != "" {
|
||||||
query = query.Where("name LIKE ?", "%"+name+"%")
|
query = query.Where("name LIKE ?", "%"+name+"%")
|
||||||
}
|
}
|
||||||
|
if agentID != nil {
|
||||||
|
query = query.Where("agent_id = ?", *agentID)
|
||||||
|
}
|
||||||
|
|
||||||
query.Count(&total)
|
query.Count(&total)
|
||||||
query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks)
|
query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks)
|
||||||
|
|||||||
@@ -55,11 +55,12 @@ export const api = {
|
|||||||
request('/auth/register', { method: 'POST', body: JSON.stringify(data) })
|
request('/auth/register', { method: 'POST', body: JSON.stringify(data) })
|
||||||
},
|
},
|
||||||
tasks: {
|
tasks: {
|
||||||
list: (params?: { page?: number; page_size?: number; name?: string }) => {
|
list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: number }) => {
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (params?.page) query.set('page', String(params.page))
|
if (params?.page) query.set('page', String(params.page))
|
||||||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||||||
if (params?.name) query.set('name', params.name)
|
if (params?.name) query.set('name', params.name)
|
||||||
|
if (params?.agent_id) query.set('agent_id', String(params.agent_id))
|
||||||
return request<TaskListResponse>(`/tasks?${query}`)
|
return request<TaskListResponse>(`/tasks?${query}`)
|
||||||
},
|
},
|
||||||
create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'
|
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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { RefreshCw, Trash2, Edit, Copy, Server, Search, Download, RotateCw, Plus, Ticket, Power, PowerOff } from 'lucide-vue-next'
|
import { RefreshCw, Trash2, Edit, Copy, Server, Search, Download, RotateCw, Plus, Ticket, Power, PowerOff, ListTodo } from 'lucide-vue-next'
|
||||||
import { api, type Agent, type AgentRegCode } from '@/api'
|
import { api, type Agent, type AgentRegCode } from '@/api'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import TextOverflow from '@/components/TextOverflow.vue'
|
import TextOverflow from '@/components/TextOverflow.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const agents = ref<Agent[]>([])
|
const agents = ref<Agent[]>([])
|
||||||
const regCodes = ref<AgentRegCode[]>([])
|
const regCodes = ref<AgentRegCode[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -121,6 +124,10 @@ async function forceUpdate(agent: Agent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function viewTasks(agent: Agent) {
|
||||||
|
router.push({ path: '/tasks', query: { agent_id: String(agent.id) } })
|
||||||
|
}
|
||||||
|
|
||||||
function copyRegCode(code: string) {
|
function copyRegCode(code: string) {
|
||||||
navigator.clipboard.writeText(code)
|
navigator.clipboard.writeText(code)
|
||||||
toast.success('已复制')
|
toast.success('已复制')
|
||||||
@@ -222,7 +229,7 @@ onUnmounted(() => {
|
|||||||
<span class="w-28">构建时间</span>
|
<span class="w-28">构建时间</span>
|
||||||
<span class="w-36">心跳时间</span>
|
<span class="w-36">心跳时间</span>
|
||||||
<span class="flex-1">描述</span>
|
<span class="flex-1">描述</span>
|
||||||
<span class="w-28 text-center">操作</span>
|
<span class="w-32 text-center">操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y min-w-[900px]">
|
<div class="divide-y min-w-[900px]">
|
||||||
<div v-if="filteredAgents.length === 0" class="text-center py-8 text-muted-foreground">
|
<div v-if="filteredAgents.length === 0" class="text-center py-8 text-muted-foreground">
|
||||||
@@ -248,11 +255,14 @@ onUnmounted(() => {
|
|||||||
<span class="flex-1 text-sm text-muted-foreground truncate">
|
<span class="flex-1 text-sm text-muted-foreground truncate">
|
||||||
<TextOverflow :text="agent.description || '-'" title="描述" />
|
<TextOverflow :text="agent.description || '-'" title="描述" />
|
||||||
</span>
|
</span>
|
||||||
<span class="w-28 flex justify-center gap-1">
|
<span class="w-32 flex justify-center gap-1">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleEnabled(agent)" :title="agent.enabled ? '禁用' : '启用'">
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleEnabled(agent)" :title="agent.enabled ? '禁用' : '启用'">
|
||||||
<Power v-if="agent.enabled" class="h-3.5 w-3.5 text-green-600" />
|
<Power v-if="agent.enabled" class="h-3.5 w-3.5 text-green-600" />
|
||||||
<PowerOff v-else class="h-3.5 w-3.5 text-gray-400" />
|
<PowerOff v-else class="h-3.5 w-3.5 text-gray-400" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewTasks(agent)" title="查看任务">
|
||||||
|
<ListTodo class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="forceUpdate(agent)" title="强制更新">
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="forceUpdate(agent)" title="强制更新">
|
||||||
<RotateCw class="h-3.5 w-3.5" />
|
<RotateCw class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -98,6 +98,13 @@ watch(() => props.open, async (val) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 切换执行位置时,如果从本地切换到 Agent,清空工作目录
|
||||||
|
watch(selectedAgentId, (newVal, oldVal) => {
|
||||||
|
if (oldVal === 'local' && newVal !== 'local') {
|
||||||
|
form.value.work_dir = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
const [envs, agents] = await Promise.all([
|
const [envs, agents] = await Promise.all([
|
||||||
@@ -159,7 +166,7 @@ async function save() {
|
|||||||
<Label class="sm:text-right text-sm">工作目录</Label>
|
<Label class="sm:text-right text-sm">工作目录</Label>
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-3">
|
||||||
<DirTreeSelect v-if="selectedAgentId === 'local'" :model-value="form.work_dir || ''" @update:model-value="v => form.work_dir = v" />
|
<DirTreeSelect v-if="selectedAgentId === 'local'" :model-value="form.work_dir || ''" @update:model-value="v => form.work_dir = v" />
|
||||||
<Input v-else v-model="form.work_dir" placeholder="Agent 上的工作目录(可选)" class="h-8 text-sm" />
|
<Input v-else v-model="form.work_dir" placeholder="工作目录(可选)" class="h-8 text-sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-3">
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import TaskDialog from './TaskDialog.vue'
|
import TaskDialog from './TaskDialog.vue'
|
||||||
import RepoDialog from './RepoDialog.vue'
|
import RepoDialog from './RepoDialog.vue'
|
||||||
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor } from 'lucide-vue-next'
|
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X } from 'lucide-vue-next'
|
||||||
import { api, type Task, type Agent } from '@/api'
|
import { api, type Task, type Agent } from '@/api'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import TextOverflow from '@/components/TextOverflow.vue'
|
import TextOverflow from '@/components/TextOverflow.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const { pageSize } = useSiteSettings()
|
const { pageSize } = useSiteSettings()
|
||||||
|
|
||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
@@ -26,6 +27,7 @@ const showDeleteDialog = ref(false)
|
|||||||
const deleteTaskId = ref<number | null>(null)
|
const deleteTaskId = ref<number | null>(null)
|
||||||
|
|
||||||
const filterName = ref('')
|
const filterName = ref('')
|
||||||
|
const filterAgentId = ref<number | null>(null)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -37,6 +39,13 @@ const agentMap = computed(() => {
|
|||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 当前筛选的 Agent 名称
|
||||||
|
const filterAgentName = computed(() => {
|
||||||
|
if (!filterAgentId.value) return ''
|
||||||
|
const agent = agentMap.value[filterAgentId.value]
|
||||||
|
return agent ? agent.name : `Agent #${filterAgentId.value}`
|
||||||
|
})
|
||||||
|
|
||||||
// 获取任务执行位置名称
|
// 获取任务执行位置名称
|
||||||
function getExecutorName(task: Task): string {
|
function getExecutorName(task: Task): string {
|
||||||
if (!task.agent_id) return '本地'
|
if (!task.agent_id) return '本地'
|
||||||
@@ -53,7 +62,12 @@ function getExecutorStatus(task: Task): 'local' | 'online' | 'offline' {
|
|||||||
|
|
||||||
async function loadTasks() {
|
async function loadTasks() {
|
||||||
try {
|
try {
|
||||||
const res = await api.tasks.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined })
|
const res = await api.tasks.list({
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
name: filterName.value || undefined,
|
||||||
|
agent_id: filterAgentId.value || undefined
|
||||||
|
})
|
||||||
tasks.value = res.data
|
tasks.value = res.data
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
} catch { toast.error('加载任务失败') }
|
} catch { toast.error('加载任务失败') }
|
||||||
@@ -78,6 +92,13 @@ function handlePageChange(page: number) {
|
|||||||
loadTasks()
|
loadTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAgentFilter() {
|
||||||
|
filterAgentId.value = null
|
||||||
|
router.replace({ query: {} })
|
||||||
|
currentPage.value = 1
|
||||||
|
loadTasks()
|
||||||
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
editingTask.value = { name: '', command: '', type: 'task', 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: '' }
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
@@ -136,9 +157,24 @@ function getTaskTypeTitle(type: string) {
|
|||||||
return type === 'repo' ? '仓库同步' : '普通任务'
|
return type === 'repo' ? '仓库同步' : '普通任务'
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
// 先加载 agents,再处理 URL 参数
|
||||||
|
await loadAgents()
|
||||||
|
|
||||||
|
// 从 URL 参数读取 agent_id
|
||||||
|
const agentIdParam = route.query.agent_id
|
||||||
|
if (agentIdParam) {
|
||||||
|
filterAgentId.value = Number(agentIdParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTasks()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听路由参数变化
|
||||||
|
watch(() => route.query.agent_id, (newVal) => {
|
||||||
|
filterAgentId.value = newVal ? Number(newVal) : null
|
||||||
|
currentPage.value = 1
|
||||||
loadTasks()
|
loadTasks()
|
||||||
loadAgents()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -154,6 +190,11 @@ onMounted(() => {
|
|||||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full sm:w-56 text-sm" @input="handleSearch" />
|
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full sm:w-56 text-sm" @input="handleSearch" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="filterAgentId" class="flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm">
|
||||||
|
<Server class="h-3.5 w-3.5" />
|
||||||
|
<span>{{ filterAgentName }}</span>
|
||||||
|
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
|
||||||
|
</div>
|
||||||
<Button variant="outline" @click="openCreateRepo" class="shrink-0">
|
<Button variant="outline" @click="openCreateRepo" class="shrink-0">
|
||||||
<GitBranch class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">仓库同步</span>
|
<GitBranch class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">仓库同步</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user