mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(team): admin REST API for teams, membership and the task board
This commit is contained in:
parent
8fa10e3769
commit
5e380a1b3c
@ -0,0 +1,307 @@
|
||||
package vip.mate.team.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
import vip.mate.agent.repository.AgentMapper;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.team.model.AgentTeamEntity;
|
||||
import vip.mate.team.model.AgentTeamMemberEntity;
|
||||
import vip.mate.team.model.TeamTaskCommentEntity;
|
||||
import vip.mate.team.model.TeamTaskCreateCommand;
|
||||
import vip.mate.team.model.TeamTaskEntity;
|
||||
import vip.mate.team.model.TeamTaskStatus;
|
||||
import vip.mate.team.service.TeamAnnounceService;
|
||||
import vip.mate.team.service.TeamDispatchService;
|
||||
import vip.mate.team.service.TeamService;
|
||||
import vip.mate.team.service.TeamTaskService;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Admin REST surface for agent teams: team/membership CRUD, the shared task
|
||||
* board, and the human-in-the-loop approve / reject / retry actions.
|
||||
*
|
||||
* All Long ids serialize as JSON strings (global Jackson config) and inbound
|
||||
* bodies accept both string and numeric forms, keeping Snowflake ids intact
|
||||
* across the JS frontend.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Tag(name = "Agent 团队管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/teams")
|
||||
@RequiredArgsConstructor
|
||||
public class TeamController {
|
||||
|
||||
private final TeamService teamService;
|
||||
private final TeamTaskService taskService;
|
||||
private final TeamDispatchService dispatchService;
|
||||
private final TeamAnnounceService announceService;
|
||||
private final AgentMapper agentMapper;
|
||||
|
||||
// ==================== team CRUD ====================
|
||||
|
||||
@Operation(summary = "团队列表")
|
||||
@GetMapping
|
||||
public R<List<TeamVO>> list() {
|
||||
return R.ok(teamService.listTeams().stream().map(this::toVO).toList());
|
||||
}
|
||||
|
||||
@Operation(summary = "团队详情(含成员)")
|
||||
@GetMapping("/{id}")
|
||||
public R<TeamDetailVO> get(@PathVariable Long id) {
|
||||
AgentTeamEntity team = teamService.getTeam(id);
|
||||
if (team == null) {
|
||||
return R.fail("team not found");
|
||||
}
|
||||
List<MemberVO> members = teamService.listMembers(id).stream()
|
||||
.map(m -> {
|
||||
AgentEntity agent = agentMapper.selectById(m.getAgentId());
|
||||
return new MemberVO(m.getAgentId(),
|
||||
agent != null && agent.getName() != null ? agent.getName()
|
||||
: String.valueOf(m.getAgentId()),
|
||||
m.getRole(),
|
||||
agent != null ? agent.getIcon() : null);
|
||||
})
|
||||
.toList();
|
||||
return R.ok(new TeamDetailVO(toVO(team), members));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建团队")
|
||||
@PostMapping
|
||||
public R<TeamVO> create(@RequestBody CreateTeamRequest req, Principal principal) {
|
||||
AgentTeamEntity team = teamService.createTeam(req.getName(), req.getDescription(),
|
||||
req.getLeadAgentId(), req.getMemberAgentIds(),
|
||||
principal != null ? principal.getName() : "admin");
|
||||
return R.ok(toVO(team));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新团队")
|
||||
@PutMapping("/{id}")
|
||||
public R<TeamVO> update(@PathVariable Long id, @RequestBody UpdateTeamRequest req) {
|
||||
return R.ok(toVO(teamService.updateTeam(id, req.getName(), req.getDescription(),
|
||||
req.getSettings())));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除团队")
|
||||
@DeleteMapping("/{id}")
|
||||
public R<Void> delete(@PathVariable Long id) {
|
||||
teamService.deleteTeam(id);
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
// ==================== membership ====================
|
||||
|
||||
@Operation(summary = "添加成员")
|
||||
@PostMapping("/{id}/members")
|
||||
public R<Void> addMember(@PathVariable Long id, @RequestBody MemberRequest req) {
|
||||
teamService.addMember(id, req.getAgentId(), req.getRole());
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
@Operation(summary = "移除成员")
|
||||
@DeleteMapping("/{id}/members/{agentId}")
|
||||
public R<Void> removeMember(@PathVariable Long id, @PathVariable Long agentId) {
|
||||
teamService.removeMember(id, agentId);
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
// ==================== task board ====================
|
||||
|
||||
@Operation(summary = "任务板列表")
|
||||
@GetMapping("/{id}/tasks")
|
||||
public R<List<TaskVO>> listTasks(@PathVariable Long id,
|
||||
@RequestParam(required = false) List<String> status) {
|
||||
return R.ok(taskService.listTasks(id, status).stream().map(this::toTaskVO).toList());
|
||||
}
|
||||
|
||||
@Operation(summary = "任务详情(含评论)")
|
||||
@GetMapping("/{id}/tasks/{taskId}")
|
||||
public R<TaskDetailVO> getTask(@PathVariable Long id, @PathVariable Long taskId) {
|
||||
TeamTaskEntity task = requireTask(id, taskId);
|
||||
return R.ok(new TaskDetailVO(toTaskVO(task), taskService.listComments(taskId)));
|
||||
}
|
||||
|
||||
@Operation(summary = "手动创建任务")
|
||||
@PostMapping("/{id}/tasks")
|
||||
public R<TaskVO> createTask(@PathVariable Long id, @RequestBody CreateTaskRequest req,
|
||||
Principal principal) {
|
||||
TeamTaskEntity task = taskService.createTask(TeamTaskCreateCommand.builder()
|
||||
.teamId(id)
|
||||
.subject(req.getSubject())
|
||||
.description(req.getDescription())
|
||||
.assigneeAgentId(req.getAssigneeAgentId())
|
||||
.priority(req.getPriority())
|
||||
.blockedBy(req.getBlockedBy())
|
||||
.requireApproval(Boolean.TRUE.equals(req.getRequireApproval()))
|
||||
.username(principal != null ? principal.getName() : null)
|
||||
.channel("dashboard")
|
||||
.build());
|
||||
if (TeamTaskStatus.PENDING.equals(task.getStatus())) {
|
||||
dispatchService.requestDispatch(id);
|
||||
}
|
||||
return R.ok(toTaskVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "批准 in_review 任务")
|
||||
@PostMapping("/{id}/tasks/{taskId}/approve")
|
||||
public R<TaskVO> approve(@PathVariable Long id, @PathVariable Long taskId) {
|
||||
requireTask(id, taskId);
|
||||
List<Long> released = taskService.approveTask(taskId);
|
||||
if (!released.isEmpty()) {
|
||||
dispatchService.requestDispatch(id);
|
||||
}
|
||||
return R.ok(toTaskVO(taskService.getTask(taskId)));
|
||||
}
|
||||
|
||||
@Operation(summary = "驳回 in_review 任务")
|
||||
@PostMapping("/{id}/tasks/{taskId}/reject")
|
||||
public R<TaskVO> reject(@PathVariable Long id, @PathVariable Long taskId,
|
||||
@RequestBody(required = false) ReasonRequest req) {
|
||||
requireTask(id, taskId);
|
||||
taskService.rejectTask(taskId, req == null ? null : req.getReason());
|
||||
TeamTaskEntity task = taskService.getTask(taskId);
|
||||
// The lead must hear about the rejection to re-plan or retry.
|
||||
announceService.announceTaskSettled(task);
|
||||
dispatchService.requestDispatch(id);
|
||||
return R.ok(toTaskVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "重试 failed/stale 任务")
|
||||
@PostMapping("/{id}/tasks/{taskId}/retry")
|
||||
public R<TaskVO> retry(@PathVariable Long id, @PathVariable Long taskId) {
|
||||
requireTask(id, taskId);
|
||||
if (!taskService.retryTask(taskId)) {
|
||||
return R.fail("only failed or stale tasks can be retried");
|
||||
}
|
||||
dispatchService.requestDispatch(id);
|
||||
return R.ok(toTaskVO(taskService.getTask(taskId)));
|
||||
}
|
||||
|
||||
@Operation(summary = "取消任务")
|
||||
@PostMapping("/{id}/tasks/{taskId}/cancel")
|
||||
public R<TaskVO> cancel(@PathVariable Long id, @PathVariable Long taskId,
|
||||
@RequestBody(required = false) ReasonRequest req) {
|
||||
requireTask(id, taskId);
|
||||
List<Long> released = taskService.cancelTask(taskId, req == null ? null : req.getReason());
|
||||
if (!released.isEmpty()) {
|
||||
dispatchService.requestDispatch(id);
|
||||
}
|
||||
return R.ok(toTaskVO(taskService.getTask(taskId)));
|
||||
}
|
||||
|
||||
@Operation(summary = "添加评论")
|
||||
@PostMapping("/{id}/tasks/{taskId}/comments")
|
||||
public R<Void> comment(@PathVariable Long id, @PathVariable Long taskId,
|
||||
@RequestBody CommentRequest req, Principal principal) {
|
||||
requireTask(id, taskId);
|
||||
taskService.addComment(taskId, TeamTaskService.AUTHOR_USER,
|
||||
principal != null ? principal.getName() : "admin",
|
||||
TeamTaskService.COMMENT_NOTE, req.getContent());
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
@Operation(summary = "任务状态统计(看板列头)")
|
||||
@GetMapping("/{id}/tasks/stats")
|
||||
public R<Map<String, Long>> taskStats(@PathVariable Long id) {
|
||||
return R.ok(taskService.listTasks(id, null).stream()
|
||||
.collect(Collectors.groupingBy(TeamTaskEntity::getStatus, Collectors.counting())));
|
||||
}
|
||||
|
||||
// ==================== helpers / DTOs ====================
|
||||
|
||||
private TeamTaskEntity requireTask(Long teamId, Long taskId) {
|
||||
TeamTaskEntity task = taskService.getTask(taskId);
|
||||
if (task == null || !task.getTeamId().equals(teamId)) {
|
||||
throw new IllegalArgumentException("task not found on this team's board");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private TeamVO toVO(AgentTeamEntity team) {
|
||||
long memberCount = teamService.listMembers(team.getId()).size();
|
||||
AgentEntity lead = agentMapper.selectById(team.getLeadAgentId());
|
||||
return new TeamVO(team,
|
||||
lead != null && lead.getName() != null ? lead.getName()
|
||||
: String.valueOf(team.getLeadAgentId()),
|
||||
lead != null ? lead.getIcon() : null,
|
||||
memberCount);
|
||||
}
|
||||
|
||||
private TaskVO toTaskVO(TeamTaskEntity task) {
|
||||
return new TaskVO(task,
|
||||
agentName(task.getAssigneeAgentId()),
|
||||
task.getOwnerAgentId() == null ? null : agentName(task.getOwnerAgentId()));
|
||||
}
|
||||
|
||||
private String agentName(Long agentId) {
|
||||
if (agentId == null) {
|
||||
return null;
|
||||
}
|
||||
AgentEntity agent = agentMapper.selectById(agentId);
|
||||
return agent != null && agent.getName() != null ? agent.getName() : String.valueOf(agentId);
|
||||
}
|
||||
|
||||
public record TeamVO(AgentTeamEntity team, String leadName, String leadIcon, long memberCount) {
|
||||
}
|
||||
|
||||
public record TeamDetailVO(TeamVO team, List<MemberVO> members) {
|
||||
}
|
||||
|
||||
public record MemberVO(Long agentId, String name, String role, String icon) {
|
||||
}
|
||||
|
||||
public record TaskVO(TeamTaskEntity task, String assigneeName, String ownerName) {
|
||||
}
|
||||
|
||||
public record TaskDetailVO(TaskVO task, List<TeamTaskCommentEntity> comments) {
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateTeamRequest {
|
||||
private String name;
|
||||
private String description;
|
||||
private Long leadAgentId;
|
||||
private List<Long> memberAgentIds;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateTeamRequest {
|
||||
private String name;
|
||||
private String description;
|
||||
private String settings;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MemberRequest {
|
||||
private Long agentId;
|
||||
private String role;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateTaskRequest {
|
||||
private String subject;
|
||||
private String description;
|
||||
private Long assigneeAgentId;
|
||||
private Integer priority;
|
||||
private List<Long> blockedBy;
|
||||
private Boolean requireApproval;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ReasonRequest {
|
||||
private String reason;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CommentRequest {
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user