diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java new file mode 100644 index 00000000..3ff9cde4 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiAdminController.java @@ -0,0 +1,81 @@ +package vip.mate.wiki.controller; + +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.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import vip.mate.wiki.job.WikiChunkTokenBackfillJob; +import vip.mate.wiki.service.WikiOverviewService; +import vip.mate.wiki.service.WikiScaffoldService; + +import java.util.HashMap; +import java.util.Map; + +/** + * RFC-051 follow-up: small set of operator-facing endpoints for things the + * scheduled jobs / event hooks normally handle automatically. Useful when the + * cron hasn't fired yet (fresh upgrade), the auto-rebuild was skipped, or you + * just want to force-refresh during debugging. + * + *

All endpoints are idempotent and synchronous. + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/wiki/admin") +@RequiredArgsConstructor +@Tag(name = "Wiki Admin", description = "Operator endpoints for system pages and backfill jobs") +public class WikiAdminController { + + private final WikiScaffoldService scaffoldService; + + /** Optional so the controller can boot in environments where the rebuilder isn't wired (e.g. minimal tests). */ + @Autowired(required = false) + private WikiOverviewService overviewService; + + @Autowired(required = false) + private WikiChunkTokenBackfillJob backfillJob; + + @Operation(summary = "Ensure overview/log scaffold + rebuild overview stats now", + description = "Idempotent. Use after manual data imports or when stats look stale.") + @PostMapping("/kb/{kbId}/rebuild-overview") + public ResponseEntity> rebuildOverview(@PathVariable Long kbId) { + Map body = new HashMap<>(); + scaffoldService.ensureScaffold(kbId); + if (overviewService != null) { + overviewService.rebuild(kbId); + body.put("rebuilt", true); + } else { + body.put("rebuilt", false); + body.put("note", "Overview service not wired; only scaffold ensured"); + } + body.put("kbId", kbId); + return ResponseEntity.ok(body); + } + + @Operation(summary = "Force-run the token-count backfill batch now", + description = "Picks up to BATCH_SIZE chunks with token_count IS NULL and fills them. " + + "Returns the pending count after the batch so callers can poll.") + @PostMapping("/backfill-tokens") + public ResponseEntity> backfillTokens() { + Map body = new HashMap<>(); + if (backfillJob == null) { + body.put("ok", false); + body.put("note", "Backfill job not wired"); + return ResponseEntity.ok(body); + } + long beforePending = backfillJob.pendingCount(); + backfillJob.runOnce(); + long afterPending = backfillJob.pendingCount(); + body.put("ok", true); + body.put("pendingBefore", beforePending); + body.put("pendingAfter", afterPending); + body.put("filledThisBatch", Math.max(0, beforePending - afterPending)); + return ResponseEntity.ok(body); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiChunkTokenBackfillJob.java b/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiChunkTokenBackfillJob.java index de3f556a..83e4e931 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiChunkTokenBackfillJob.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/job/WikiChunkTokenBackfillJob.java @@ -39,6 +39,16 @@ public class WikiChunkTokenBackfillJob { private final WikiChunkMapper chunkMapper; + /** + * RFC-051 follow-up: count chunks still missing a token estimate. + * Used by the admin endpoint to decide whether a manual rerun is worthwhile. + */ + public long pendingCount() { + return chunkMapper.selectCount( + new LambdaQueryWrapper() + .isNull(WikiChunkEntity::getTokenCount)); + } + @Async @Scheduled(cron = "${mate.wiki.chunk-token-backfill-cron:0 */30 * * * ?}") public void runOnce() { diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiCompileService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiCompileService.java index 09adc5a4..8533c934 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiCompileService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiCompileService.java @@ -58,8 +58,22 @@ public class WikiCompileService { @Autowired(required = false) private WikiLogService logService; + /** + * Compile outcome. + *

+ */ public record CompileResult(Long pageId, String slug, String title, int evidenceChunkCount, - boolean created) {} + boolean created) { + + public static CompileResult noEvidence() { + return new CompileResult(null, null, null, 0, false); + } + } /** * Compile or update a single page on the topic. @@ -79,7 +93,10 @@ public class WikiCompileService { // 1. Retrieve evidence chunks via semantic search (hybrid retriever). List hits = hybridRetriever.searchChunks(kbId, topic, cap); if (hits.isEmpty()) { - throw new IllegalStateException("No evidence chunks found for topic: " + topic); + // Structured "nothing matched" result rather than throw — lets the + // tool surface respond with a clean message instead of a stack trace. + log.info("[WikiCompile] No evidence chunks for topic='{}' kbId={}", topic, kbId); + return CompileResult.noEvidence(); } List evidenceChunkIds = new ArrayList<>(hits.size()); diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java index 75c5caf6..3d8e7d5f 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java @@ -390,8 +390,21 @@ public class WikiTool { try { WikiCompileService.CompileResult res = compileService.compilePage(kbId, topic, slug, maxEvidenceChunks); + // RFC-051 follow-up: distinguish "no source material" from a hard error + // so the agent can decide whether to retry, fall back to search, or tell + // the user there's nothing on this topic. + if (res.evidenceChunkCount() == 0) { + return JSONUtil.createObj() + .set("ok", true) + .set("compiled", false) + .set("reason", "no_evidence") + .set("message", "No chunks matched the topic. Try wiki_search_pages, or upload source material first.") + .set("evidenceChunks", 0) + .toString(); + } return JSONUtil.createObj() .set("ok", true) + .set("compiled", true) .set("slug", res.slug()) .set("title", res.title()) .set("evidenceChunks", res.evidenceChunkCount())