mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(ui): agent teams page with team management, kanban board and role-aware member views
This commit is contained in:
parent
5e380a1b3c
commit
bc867f0cbb
@ -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.
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: '核心',
|
||||
|
||||
@ -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.
|
||||
|
||||
103
mateclaw-ui/src/stores/useTeamStore.ts
Normal file
103
mateclaw-ui/src/stores/useTeamStore.ts
Normal file
@ -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<TeamVO[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const currentTeam = ref<TeamVO | null>(null)
|
||||
const members = ref<TeamMemberVO[]>([])
|
||||
const tasks = ref<TeamTaskVO[]>([])
|
||||
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 }
|
||||
7
mateclaw-ui/src/types/components.d.ts
vendored
7
mateclaw-ui/src/types/components.d.ts
vendored
@ -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']
|
||||
|
||||
1385
mateclaw-ui/src/views/Teams.vue
Normal file
1385
mateclaw-ui/src/views/Teams.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -480,6 +480,12 @@ const navGroups = computed(() => [
|
||||
icon: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>`,
|
||||
requiredCapability: 'manage:agents',
|
||||
},
|
||||
{
|
||||
path: '/teams',
|
||||
label: t('nav.teams'),
|
||||
icon: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
|
||||
requiredCapability: 'manage:agents',
|
||||
},
|
||||
{
|
||||
path: '/wiki',
|
||||
label: t('nav.wiki'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user