mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +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 } }),
|
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 ====================
|
// ==================== Wiki Knowledge Base ====================
|
||||||
// One row in the cross-KB failure center. ids are strings (global Long→String
|
// One row in the cross-KB failure center. ids are strings (global Long→String
|
||||||
// Jackson config) to avoid Snowflake precision loss.
|
// Jackson config) to avoid Snowflake precision loss.
|
||||||
|
|||||||
@ -5,6 +5,61 @@ export default {
|
|||||||
router: {
|
router: {
|
||||||
chunkLoadFailed: 'Failed to load page resources. Check your network and try again.',
|
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: {
|
common: {
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
@ -538,6 +593,7 @@ export default {
|
|||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
chat: 'Chat',
|
chat: 'Chat',
|
||||||
control: 'Control',
|
control: 'Control',
|
||||||
|
teams: 'Teams',
|
||||||
channels: 'Channels',
|
channels: 'Channels',
|
||||||
sessions: 'Sessions',
|
sessions: 'Sessions',
|
||||||
agent: 'Agent',
|
agent: 'Agent',
|
||||||
|
|||||||
@ -5,6 +5,61 @@ export default {
|
|||||||
router: {
|
router: {
|
||||||
chunkLoadFailed: '页面资源加载失败,请检查网络后重试',
|
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: {
|
common: {
|
||||||
save: '保存',
|
save: '保存',
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
@ -538,6 +593,7 @@ export default {
|
|||||||
dashboard: '仪表盘',
|
dashboard: '仪表盘',
|
||||||
chat: '对话',
|
chat: '对话',
|
||||||
control: '控制台',
|
control: '控制台',
|
||||||
|
teams: '团队',
|
||||||
channels: '渠道',
|
channels: '渠道',
|
||||||
sessions: '会话',
|
sessions: '会话',
|
||||||
core: '核心',
|
core: '核心',
|
||||||
|
|||||||
@ -47,6 +47,12 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/AgentCreateWizard.vue'),
|
component: () => import('@/views/AgentCreateWizard.vue'),
|
||||||
meta: { title: 'Create Agent', requiredCapability: 'manage:agents' },
|
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.
|
// Live runtime view folded into the Agents page as a sub-view.
|
||||||
// Kept as a redirect so old links / bookmarks still resolve.
|
// 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 */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ElAlert: typeof import('element-plus/es/components/alert/index')['ElAlert']
|
||||||
ElButton: typeof import('element-plus/es/components/button/index')['ElButton']
|
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']
|
ElConfigProvider: typeof import('element-plus/es/components/config-provider/index')['ElConfigProvider']
|
||||||
ElDatePicker: typeof import('element-plus/es/components/date-picker/index')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es/components/date-picker/index')['ElDatePicker']
|
||||||
ElDialog: typeof import('element-plus/es/components/dialog/index')['ElDialog']
|
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']
|
ElDropdownItem: typeof import('element-plus/es/components/dropdown/index')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es/components/dropdown/index')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es/components/dropdown/index')['ElDropdownMenu']
|
||||||
ElEmpty: typeof import('element-plus/es/components/empty/index')['ElEmpty']
|
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']
|
ElIcon: typeof import('element-plus/es/components/icon/index')['ElIcon']
|
||||||
ElImageViewer: typeof import('element-plus/es/components/image-viewer/index')['ElImageViewer']
|
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']
|
ElOption: typeof import('element-plus/es/components/select/index')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es/components/pagination/index')['ElPagination']
|
ElPagination: typeof import('element-plus/es/components/pagination/index')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es/components/popover/index')['ElPopover']
|
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']
|
ElSelect: typeof import('element-plus/es/components/select/index')['ElSelect']
|
||||||
ElSkeleton: typeof import('element-plus/es/components/skeleton/index')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es/components/skeleton/index')['ElSkeleton']
|
||||||
ElTable: typeof import('element-plus/es/components/table/index')['ElTable']
|
ElTable: typeof import('element-plus/es/components/table/index')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es/components/table/index')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es/components/table/index')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es/components/tabs/index')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es/components/tabs/index')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es/components/tabs/index')['ElTabs']
|
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']
|
ElTooltip: typeof import('element-plus/es/components/tooltip/index')['ElTooltip']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
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>`,
|
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',
|
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',
|
path: '/wiki',
|
||||||
label: t('nav.wiki'),
|
label: t('nav.wiki'),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user