mateclaw/mateclaw-server/src/main/java/vip/mate/notification/NotificationController.java
倪程伟 fbbd1218e8
feat(wiki): KB processing-failure visibility (error-code chain + silent sub-step alerts + cross-KB failure center)
Propagates structured error codes through the KB processing pipeline, surfaces silent sub-step warnings as a non-failure warning state, and adds a cross-KB failure center for aggregated visibility.
2026-06-28 13:07:34 +08:00

81 lines
3.3 KiB
Java

package vip.mate.notification;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.mate.agent.runtime.AgentRuntimeAggregator;
import vip.mate.approval.ApprovalWorkflowService;
import vip.mate.common.result.R;
import vip.mate.exception.MateClawException;
import vip.mate.wiki.service.WikiRawMaterialService;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Aggregate counts that drive global UI attention signals (sidebar badges,
* future notification center).
*
* <p>Designed so the frontend can poll a single endpoint instead of fan-out
* to every domain service. Fields with no settled "is it actually a problem"
* semantics (failed crons / down channels / down MCP servers) are returned
* as zero placeholders so the wire shape is stable and later phases can
* populate them without bumping the contract.
*/
@Slf4j
@Tag(name = "Notifications")
@RestController
@RequestMapping("/api/v1/notifications")
@RequiredArgsConstructor
public class NotificationController {
private final ApprovalWorkflowService approvalWorkflowService;
private final AgentRuntimeAggregator agentRuntimeAggregator;
private final WikiRawMaterialService wikiRawMaterialService;
@Operation(summary = "Aggregated counts for the sidebar attention badges")
@GetMapping("/summary")
public R<Map<String, Object>> summary(Authentication auth) {
boolean admin = isAdmin(auth);
// Cast to int — counts won't exceed Integer.MAX_VALUE in practice
// and the project's global Jackson config serializes Long as a string
// (for ID precision), which would break the numeric UI badge.
int pendingApprovals = (int) Math.min(Integer.MAX_VALUE, approvalWorkflowService.countPendingFromDb());
int stuckAgents = admin
? agentRuntimeAggregator.snapshot().summary().stuck()
: 0;
// Cross-KB Wiki ingest failures/degradations — admin-only, mirroring
// stuckAgents (the list view it links to spans every workspace).
int failedWikiJobs = admin
? (int) Math.min(Integer.MAX_VALUE, wikiRawMaterialService.countFailures())
: 0;
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("pendingApprovals", pendingApprovals);
payload.put("stuckAgents", stuckAgents);
payload.put("failedWikiJobs", failedWikiJobs);
// Reserved fields — wire shape stays stable so the frontend doesn't
// need a fan-out when these get real semantics later.
payload.put("failedCrons", 0);
payload.put("downChannels", 0);
payload.put("downMcps", 0);
return R.ok(payload);
}
private boolean isAdmin(Authentication auth) {
if (auth == null) {
throw new MateClawException(401, "authentication required");
}
return auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch("ROLE_ADMIN"::equals);
}
}