feat(wiki): pluggable ingest-source SPI + source-watcher status API

This commit is contained in:
matevip 2026-05-31 07:59:34 +08:00
parent ea0ace1e49
commit c627f898ec
4 changed files with 133 additions and 9 deletions

View File

@ -61,6 +61,7 @@ public class WikiController {
private final AuditEventService auditEventService;
private final WikiPageTypeProfileService pageTypeProfileService;
private final WikiSourcePathValidator pathValidator;
private final vip.mate.wiki.service.WikiSourceWatcherService sourceWatcherService;
private final ObjectMapper objectMapper;
// ==================== Knowledge Base ====================
@ -302,6 +303,47 @@ public class WikiController {
return R.ok(response);
}
// ==================== Source Watcher ====================
@RequireWorkspaceRole("viewer")
@Operation(summary = "查看知识库源监听状态")
@GetMapping("/knowledge-bases/{id}/source-watcher")
public R<Map<String, Object>> getSourceWatcher(@PathVariable Long id,
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
verifyKBWorkspace(id, workspaceId);
WikiKnowledgeBaseEntity kb = kbService.getById(id);
if (kb == null) return R.fail(404, "Knowledge base not found");
vip.mate.wiki.source.WikiIngestSourceProvider provider = sourceWatcherService.providerFor(kb);
Map<String, Object> out = new LinkedHashMap<>();
out.put("watcherEnabled", properties.isWatcherEnabled());
out.put("intervalMs", properties.getWatcherIntervalMs());
out.put("sourceDirectory", kb.getSourceDirectory());
out.put("sourceType", provider != null ? provider.sourceType() : null);
out.put("availableSourceTypes", sourceWatcherService.availableSourceTypes());
out.put("active", provider != null);
return R.ok(out);
}
@RequireWorkspaceRole("member")
@Operation(summary = "手动触发一次源监听扫描")
@PostMapping("/knowledge-bases/{id}/source-watcher/scan")
public R<Map<String, Object>> triggerSourceWatcher(@PathVariable Long id,
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
verifyKBWorkspace(id, workspaceId);
WikiKnowledgeBaseEntity kb = kbService.getById(id);
if (kb == null) return R.fail(404, "Knowledge base not found");
vip.mate.wiki.source.WikiIngestSourceProvider provider = sourceWatcherService.providerFor(kb);
if (provider == null) return R.fail(400, "No source configured for this knowledge base");
WikiDirectoryScanService.ScanResult result = provider.sync(kb);
Map<String, Object> out = new LinkedHashMap<>();
out.put("sourceType", provider.sourceType());
out.put("scanned", result.scanned());
out.put("added", result.added());
out.put("skipped", result.skipped());
out.put("errors", result.errors());
return R.ok(out);
}
// ==================== Raw Materials ====================
@RequireWorkspaceRole("viewer")

View File

@ -26,15 +26,20 @@ import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
public class WikiSourceWatcherService {
private final WikiKnowledgeBaseService kbService;
private final WikiDirectoryScanService scanService;
private final WikiProperties properties;
private final java.util.List<vip.mate.wiki.source.WikiIngestSourceProvider> sourceProviders;
public WikiSourceWatcherService(WikiKnowledgeBaseService kbService,
WikiDirectoryScanService scanService,
WikiProperties properties) {
WikiProperties properties,
java.util.List<vip.mate.wiki.source.WikiIngestSourceProvider> sourceProviders) {
this.kbService = kbService;
this.scanService = scanService;
this.properties = properties;
this.sourceProviders = sourceProviders;
}
/** The registered source-provider types (filesystem ships; api/mq pluggable later). */
public java.util.List<String> availableSourceTypes() {
return sourceProviders.stream().map(vip.mate.wiki.source.WikiIngestSourceProvider::sourceType).toList();
}
/** Scheduled entry point — gated by config, serialized across instances. */
@ -58,20 +63,32 @@ public class WikiSourceWatcherService {
public int runScanCycle() {
int totalAdded = 0;
for (WikiKnowledgeBaseEntity kb : kbService.listAll()) {
String dir = kb.getSourceDirectory();
if (dir == null || dir.isBlank()) {
vip.mate.wiki.source.WikiIngestSourceProvider provider = providerFor(kb);
if (provider == null) {
continue;
}
try {
WikiDirectoryScanService.ScanResult result = scanService.scanDirectory(kb.getId(), dir);
WikiDirectoryScanService.ScanResult result = provider.sync(kb);
totalAdded += result.added();
if (!result.errors().isEmpty()) {
log.warn("[WikiWatcher] KB {} scan reported issues: {}", kb.getId(), result.errors());
log.warn("[WikiWatcher] KB {} ({}) sync reported issues: {}",
kb.getId(), provider.sourceType(), result.errors());
}
} catch (Exception e) {
log.warn("[WikiWatcher] scan failed for KB {}: {}", kb.getId(), e.getMessage());
log.warn("[WikiWatcher] sync failed for KB {} ({}): {}",
kb.getId(), provider.sourceType(), e.getMessage());
}
}
return totalAdded;
}
/** The first registered provider that supports the KB, or null. */
public vip.mate.wiki.source.WikiIngestSourceProvider providerFor(WikiKnowledgeBaseEntity kb) {
for (vip.mate.wiki.source.WikiIngestSourceProvider p : sourceProviders) {
if (p.supports(kb)) {
return p;
}
}
return null;
}
}

View File

@ -0,0 +1,37 @@
package vip.mate.wiki.source;
import org.springframework.stereotype.Component;
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
import vip.mate.wiki.service.WikiDirectoryScanService;
/**
* The built-in filesystem source: syncs a KB by scanning its configured source
* directory (path validation, symlink resolution and content-hash change
* detection live in the scan service).
*
* @author MateClaw Team
*/
@Component
public class FilesystemSourceProvider implements WikiIngestSourceProvider {
private final WikiDirectoryScanService scanService;
public FilesystemSourceProvider(WikiDirectoryScanService scanService) {
this.scanService = scanService;
}
@Override
public String sourceType() {
return "filesystem";
}
@Override
public boolean supports(WikiKnowledgeBaseEntity kb) {
return kb != null && kb.getSourceDirectory() != null && !kb.getSourceDirectory().isBlank();
}
@Override
public WikiDirectoryScanService.ScanResult sync(WikiKnowledgeBaseEntity kb) {
return scanService.scanDirectory(kb.getId(), kb.getSourceDirectory());
}
}

View File

@ -0,0 +1,28 @@
package vip.mate.wiki.source;
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
import vip.mate.wiki.service.WikiDirectoryScanService;
/**
* Pluggable source of raw material for a knowledge base. The watcher iterates
* KBs and asks each registered provider whether it {@link #supports} the KB,
* then {@link #sync}s it. The filesystem implementation ships today; API /
* message-queue sources can be added later by implementing this interface
* without touching the watcher.
*
* @author MateClaw Team
*/
public interface WikiIngestSourceProvider {
/** Stable source-type id, e.g. {@code filesystem} / {@code api} / {@code mq}. */
String sourceType();
/** Whether this provider can sync the given KB (e.g. it has the relevant config). */
boolean supports(WikiKnowledgeBaseEntity kb);
/**
* Pull new / changed material for the KB and ingest it, returning the
* scan-style result (scanned / added / skipped / errors).
*/
WikiDirectoryScanService.ScanResult sync(WikiKnowledgeBaseEntity kb);
}