From bc867f0cbb1d49aee6e8057bf5e3460f82f1bb24 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 24 Jul 2026 17:38:44 +0800 Subject: [PATCH] feat(ui): agent teams page with team management, kanban board and role-aware member views --- mateclaw-ui/src/api/index.ts | 106 ++ mateclaw-ui/src/i18n/locales/en-US.ts | 56 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 56 + mateclaw-ui/src/router/index.ts | 6 + mateclaw-ui/src/stores/useTeamStore.ts | 103 ++ mateclaw-ui/src/types/components.d.ts | 7 + mateclaw-ui/src/views/Teams.vue | 1385 +++++++++++++++++++ mateclaw-ui/src/views/layout/MainLayout.vue | 6 + 8 files changed, 1725 insertions(+) create mode 100644 mateclaw-ui/src/stores/useTeamStore.ts create mode 100644 mateclaw-ui/src/views/Teams.vue diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 0bab2d7a..ddb84063 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -806,6 +806,112 @@ export const cronJobApi = { http.get('/cron-jobs/active-runs', { params: { conversationId } }), } +// ==================== Agent Teams ==================== +// All ids are strings end-to-end (global Long→String Jackson config) — never +// coerce them to number, Snowflake ids exceed Number.MAX_SAFE_INTEGER. + +export interface AgentTeam { + id: string + name: string + description: string | null + leadAgentId: string + status: string + settings: string | null + createTime?: string +} + +export interface TeamVO { + team: AgentTeam + leadName: string | null + leadIcon?: string | null + memberCount: number +} + +export interface TeamMemberVO { + agentId: string + name: string + role: 'lead' | 'member' | 'reviewer' + icon?: string | null +} + +export interface TeamTask { + id: string + teamId: string + taskNumber: number + subject: string + description: string | null + status: string + priority: number + assigneeAgentId: string | null + ownerAgentId: string | null + blockedBy: string | null + requireApproval: boolean | null + progressPercent: number | null + progressStep: string | null + result: string | null + reason: string | null + dispatchCount: number + conversationId: string | null + leadConversationId: string | null + createTime?: string + updateTime?: string +} + +export interface TeamTaskVO { + task: TeamTask + assigneeName: string | null + ownerName: string | null +} + +export interface TeamTaskComment { + id: string + taskId: string + authorType: string + authorId: string + commentType: string + content: string + createTime?: string +} + +export const teamApi = { + list: () => http.get('/teams'), + get: (id: string) => http.get(`/teams/${id}`), + create: (data: { + name: string + description?: string + leadAgentId: string + memberAgentIds: string[] + }) => http.post('/teams', data), + update: (id: string, data: { name?: string; description?: string; settings?: string }) => + http.put(`/teams/${id}`, data), + delete: (id: string) => http.delete(`/teams/${id}`), + addMember: (id: string, agentId: string, role: string) => + http.post(`/teams/${id}/members`, { agentId, role }), + removeMember: (id: string, agentId: string) => http.delete(`/teams/${id}/members/${agentId}`), + listTasks: (id: string, status?: string[]) => + http.get(`/teams/${id}/tasks`, { params: status?.length ? { status: status.join(',') } : {} }), + getTask: (id: string, taskId: string) => http.get(`/teams/${id}/tasks/${taskId}`), + createTask: ( + id: string, + data: { + subject: string + description?: string + assigneeAgentId: string + priority?: number + blockedBy?: string[] + requireApproval?: boolean + }, + ) => http.post(`/teams/${id}/tasks`, data), + approveTask: (id: string, taskId: string) => http.post(`/teams/${id}/tasks/${taskId}/approve`), + rejectTask: (id: string, taskId: string, reason?: string) => + http.post(`/teams/${id}/tasks/${taskId}/reject`, { reason }), + retryTask: (id: string, taskId: string) => http.post(`/teams/${id}/tasks/${taskId}/retry`), + cancelTask: (id: string, taskId: string, reason?: string) => + http.post(`/teams/${id}/tasks/${taskId}/cancel`, { reason }), + commentTask: (id: string, taskId: string, content: string) => + http.post(`/teams/${id}/tasks/${taskId}/comments`, { content }), +} + // ==================== Wiki Knowledge Base ==================== // One row in the cross-KB failure center. ids are strings (global Long→String // Jackson config) to avoid Snowflake precision loss. diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 81241841..a8189131 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -5,6 +5,61 @@ export default { router: { chunkLoadFailed: 'Failed to load page resources. Check your network and try again.', }, + teams: { + kicker: 'Multi-Agent Collaboration', + title: 'Agent Teams', + subtitle: 'Multi-agent teams: the lead orchestrates, members execute', + create: 'Create Team', + empty: 'No teams yet — create a lead-orchestrated multi-agent team', + memberCount: '{count} members', + back: 'Back', + board: 'Task Board', + members: 'Members', + addMember: 'Add Member', + memberName: 'Member', + role: 'Role', + name: 'Name', + description: 'Description', + lead: 'Lead', + membersField: 'Members', + createIncomplete: 'Name, lead and at least one member are required', + pickHint: 'Click to select / deselect; the lead cannot double as a member', + roles: { + lead: 'Lead', + member: 'Member', + reviewer: 'Reviewer', + }, + noCandidates: 'No agents left to add (all are already on the team)', + deleteConfirm: 'Delete this team? Historical tasks are kept but no longer dispatched.', + cancelConfirm: 'Cancel this task?', + assignee: 'Assignee', + taskDescription: 'Description', + result: 'Result', + comments: 'Comments', + commentPlaceholder: 'Write a comment…', + approve: 'Approve', + approved: 'Approved', + reject: 'Reject', + rejectReason: 'Rejection reason (the lead will be notified)', + retry: 'Retry', + column: { + todo: 'To Do', + in_progress: 'In Progress', + in_review: 'In Review', + completed: 'Completed', + closed: 'Closed', + }, + status: { + pending: 'Pending', + blocked: 'Blocked', + in_progress: 'In Progress', + in_review: 'In Review', + completed: 'Completed', + failed: 'Failed', + cancelled: 'Cancelled', + stale: 'Stale', + }, + }, common: { save: 'Save', saving: 'Saving...', @@ -538,6 +593,7 @@ export default { dashboard: 'Dashboard', chat: 'Chat', control: 'Control', + teams: 'Teams', channels: 'Channels', sessions: 'Sessions', agent: 'Agent', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 91a6aa31..b3f511c4 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -5,6 +5,61 @@ export default { router: { chunkLoadFailed: '页面资源加载失败,请检查网络后重试', }, + teams: { + kicker: '多 Agent 协作', + title: 'Agent 团队', + subtitle: 'Lead 编排、成员执行的多 Agent 协作团队', + create: '创建团队', + empty: '还没有团队,创建一个由 Lead 编排的多 Agent 团队', + memberCount: '{count} 名成员', + back: '返回', + board: '任务板', + members: '成员', + addMember: '添加成员', + memberName: '成员', + role: '角色', + name: '名称', + description: '描述', + lead: 'Lead', + membersField: '成员', + createIncomplete: '请填写名称、Lead 和至少一名成员', + pickHint: '点击选择 / 取消;Lead 不能同时作为成员', + roles: { + lead: '负责人', + member: '成员', + reviewer: '审核员', + }, + noCandidates: '没有可添加的 Agent(都已在团队中)', + deleteConfirm: '确定删除该团队?历史任务将保留但不再派发。', + cancelConfirm: '确定取消该任务?', + assignee: '执行人', + taskDescription: '任务说明', + result: '执行结果', + comments: '评论', + commentPlaceholder: '写下评论…', + approve: '批准', + approved: '已批准', + reject: '驳回', + rejectReason: '驳回原因(将通知 Lead)', + retry: '重试', + column: { + todo: '待处理', + in_progress: '进行中', + in_review: '待审核', + completed: '已完成', + closed: '已终止', + }, + status: { + pending: '待处理', + blocked: '等待依赖', + in_progress: '进行中', + in_review: '待审核', + completed: '已完成', + failed: '失败', + cancelled: '已取消', + stale: '已过期', + }, + }, common: { save: '保存', saving: '保存中...', @@ -538,6 +593,7 @@ export default { dashboard: '仪表盘', chat: '对话', control: '控制台', + teams: '团队', channels: '渠道', sessions: '会话', core: '核心', diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts index 051514c7..2c59abd0 100644 --- a/mateclaw-ui/src/router/index.ts +++ b/mateclaw-ui/src/router/index.ts @@ -47,6 +47,12 @@ const router = createRouter({ component: () => import('@/views/AgentCreateWizard.vue'), meta: { title: 'Create Agent', requiredCapability: 'manage:agents' }, }, + { + path: 'teams', + name: 'Teams', + component: () => import('@/views/Teams.vue'), + meta: { title: 'Teams', requiredCapability: 'manage:agents' }, + }, { // Live runtime view folded into the Agents page as a sub-view. // Kept as a redirect so old links / bookmarks still resolve. diff --git a/mateclaw-ui/src/stores/useTeamStore.ts b/mateclaw-ui/src/stores/useTeamStore.ts new file mode 100644 index 00000000..580c903c --- /dev/null +++ b/mateclaw-ui/src/stores/useTeamStore.ts @@ -0,0 +1,103 @@ +import { acceptHMRUpdate, defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { teamApi } from '@/api/index' +import type { TeamMemberVO, TeamTaskComment, TeamTaskVO, TeamVO } from '@/api/index' + +/** + * Agent-team domain state: team list, the currently opened team (members + + * task board), and board polling. Ids stay strings for their entire lifecycle + * (Snowflake precision convention). + */ +export const useTeamStore = defineStore('team', () => { + const teams = ref([]) + const loading = ref(false) + + const currentTeam = ref(null) + const members = ref([]) + const tasks = ref([]) + const boardLoading = ref(false) + + /** Statuses that mean the board is still moving and worth polling. */ + const ACTIVE_STATUSES = ['pending', 'in_progress', 'in_review', 'blocked'] + + const hasActiveTasks = computed(() => + tasks.value.some((t) => ACTIVE_STATUSES.includes(t.task.status)), + ) + + async function fetchTeams() { + loading.value = true + try { + const res: any = await teamApi.list() + teams.value = res.data || [] + } catch (e) { + console.error('Failed to fetch teams', e) + } finally { + loading.value = false + } + } + + async function openTeam(teamId: string) { + const res: any = await teamApi.get(teamId) + currentTeam.value = res.data?.team || null + members.value = res.data?.members || [] + await fetchTasks(teamId) + } + + function closeTeam() { + currentTeam.value = null + members.value = [] + tasks.value = [] + } + + async function fetchTasks(teamId: string) { + boardLoading.value = true + try { + const res: any = await teamApi.listTasks(teamId) + tasks.value = res.data || [] + } catch (e) { + console.error('Failed to fetch team tasks', e) + } finally { + boardLoading.value = false + } + } + + async function createTeam(data: { + name: string + description?: string + leadAgentId: string + memberAgentIds: string[] + }) { + await teamApi.create(data) + await fetchTeams() + } + + async function deleteTeam(teamId: string) { + await teamApi.delete(teamId) + if (currentTeam.value?.team.id === teamId) { + closeTeam() + } + await fetchTeams() + } + + return { + teams, + loading, + currentTeam, + members, + tasks, + boardLoading, + hasActiveTasks, + fetchTeams, + openTeam, + closeTeam, + fetchTasks, + createTeam, + deleteTeam, + } +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useTeamStore, import.meta.hot)) +} + +export type { TeamMemberVO, TeamTaskComment, TeamTaskVO, TeamVO } diff --git a/mateclaw-ui/src/types/components.d.ts b/mateclaw-ui/src/types/components.d.ts index 14909b9f..1dc64a8d 100644 --- a/mateclaw-ui/src/types/components.d.ts +++ b/mateclaw-ui/src/types/components.d.ts @@ -11,7 +11,9 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + ElAlert: typeof import('element-plus/es/components/alert/index')['ElAlert'] ElButton: typeof import('element-plus/es/components/button/index')['ElButton'] + ElCard: typeof import('element-plus/es/components/card/index')['ElCard'] ElConfigProvider: typeof import('element-plus/es/components/config-provider/index')['ElConfigProvider'] ElDatePicker: typeof import('element-plus/es/components/date-picker/index')['ElDatePicker'] ElDialog: typeof import('element-plus/es/components/dialog/index')['ElDialog'] @@ -20,17 +22,22 @@ declare module 'vue' { ElDropdownItem: typeof import('element-plus/es/components/dropdown/index')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es/components/dropdown/index')['ElDropdownMenu'] ElEmpty: typeof import('element-plus/es/components/empty/index')['ElEmpty'] + ElForm: typeof import('element-plus/es/components/form/index')['ElForm'] + ElFormItem: typeof import('element-plus/es/components/form/index')['ElFormItem'] ElIcon: typeof import('element-plus/es/components/icon/index')['ElIcon'] ElImageViewer: typeof import('element-plus/es/components/image-viewer/index')['ElImageViewer'] + ElInput: typeof import('element-plus/es/components/input/index')['ElInput'] ElOption: typeof import('element-plus/es/components/select/index')['ElOption'] ElPagination: typeof import('element-plus/es/components/pagination/index')['ElPagination'] ElPopover: typeof import('element-plus/es/components/popover/index')['ElPopover'] + ElProgress: typeof import('element-plus/es/components/progress/index')['ElProgress'] ElSelect: typeof import('element-plus/es/components/select/index')['ElSelect'] ElSkeleton: typeof import('element-plus/es/components/skeleton/index')['ElSkeleton'] ElTable: typeof import('element-plus/es/components/table/index')['ElTable'] ElTableColumn: typeof import('element-plus/es/components/table/index')['ElTableColumn'] ElTabPane: typeof import('element-plus/es/components/tabs/index')['ElTabPane'] ElTabs: typeof import('element-plus/es/components/tabs/index')['ElTabs'] + ElTag: typeof import('element-plus/es/components/tag/index')['ElTag'] ElTooltip: typeof import('element-plus/es/components/tooltip/index')['ElTooltip'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/mateclaw-ui/src/views/Teams.vue b/mateclaw-ui/src/views/Teams.vue new file mode 100644 index 00000000..7c2c21a6 --- /dev/null +++ b/mateclaw-ui/src/views/Teams.vue @@ -0,0 +1,1385 @@ + + + + + diff --git a/mateclaw-ui/src/views/layout/MainLayout.vue b/mateclaw-ui/src/views/layout/MainLayout.vue index a195265f..27eac1f1 100644 --- a/mateclaw-ui/src/views/layout/MainLayout.vue +++ b/mateclaw-ui/src/views/layout/MainLayout.vue @@ -480,6 +480,12 @@ const navGroups = computed(() => [ icon: ``, requiredCapability: 'manage:agents', }, + { + path: '/teams', + label: t('nav.teams'), + icon: ``, + requiredCapability: 'manage:agents', + }, { path: '/wiki', label: t('nav.wiki'),