fix: agent workdir error

This commit is contained in:
engigu
2025-12-29 19:52:39 +08:00
parent 78c3707cce
commit 6c666d998c
6 changed files with 84 additions and 13 deletions
+10 -1
View File
@@ -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)
} }
+4 -1
View File
@@ -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)
+2 -1
View File
@@ -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) }),
+13 -3
View File
@@ -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>
+8 -1
View File
@@ -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">
+47 -6
View File
@@ -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>