mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(wiki): pluggable ingest-source SPI + source-watcher status API
This commit is contained in:
parent
ea0ace1e49
commit
c627f898ec
@ -61,6 +61,7 @@ public class WikiController {
|
|||||||
private final AuditEventService auditEventService;
|
private final AuditEventService auditEventService;
|
||||||
private final WikiPageTypeProfileService pageTypeProfileService;
|
private final WikiPageTypeProfileService pageTypeProfileService;
|
||||||
private final WikiSourcePathValidator pathValidator;
|
private final WikiSourcePathValidator pathValidator;
|
||||||
|
private final vip.mate.wiki.service.WikiSourceWatcherService sourceWatcherService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
// ==================== Knowledge Base ====================
|
// ==================== Knowledge Base ====================
|
||||||
@ -302,6 +303,47 @@ public class WikiController {
|
|||||||
return R.ok(response);
|
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 ====================
|
// ==================== Raw Materials ====================
|
||||||
|
|
||||||
@RequireWorkspaceRole("viewer")
|
@RequireWorkspaceRole("viewer")
|
||||||
|
|||||||
@ -26,15 +26,20 @@ import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
|||||||
public class WikiSourceWatcherService {
|
public class WikiSourceWatcherService {
|
||||||
|
|
||||||
private final WikiKnowledgeBaseService kbService;
|
private final WikiKnowledgeBaseService kbService;
|
||||||
private final WikiDirectoryScanService scanService;
|
|
||||||
private final WikiProperties properties;
|
private final WikiProperties properties;
|
||||||
|
private final java.util.List<vip.mate.wiki.source.WikiIngestSourceProvider> sourceProviders;
|
||||||
|
|
||||||
public WikiSourceWatcherService(WikiKnowledgeBaseService kbService,
|
public WikiSourceWatcherService(WikiKnowledgeBaseService kbService,
|
||||||
WikiDirectoryScanService scanService,
|
WikiProperties properties,
|
||||||
WikiProperties properties) {
|
java.util.List<vip.mate.wiki.source.WikiIngestSourceProvider> sourceProviders) {
|
||||||
this.kbService = kbService;
|
this.kbService = kbService;
|
||||||
this.scanService = scanService;
|
|
||||||
this.properties = properties;
|
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. */
|
/** Scheduled entry point — gated by config, serialized across instances. */
|
||||||
@ -58,20 +63,32 @@ public class WikiSourceWatcherService {
|
|||||||
public int runScanCycle() {
|
public int runScanCycle() {
|
||||||
int totalAdded = 0;
|
int totalAdded = 0;
|
||||||
for (WikiKnowledgeBaseEntity kb : kbService.listAll()) {
|
for (WikiKnowledgeBaseEntity kb : kbService.listAll()) {
|
||||||
String dir = kb.getSourceDirectory();
|
vip.mate.wiki.source.WikiIngestSourceProvider provider = providerFor(kb);
|
||||||
if (dir == null || dir.isBlank()) {
|
if (provider == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
WikiDirectoryScanService.ScanResult result = scanService.scanDirectory(kb.getId(), dir);
|
WikiDirectoryScanService.ScanResult result = provider.sync(kb);
|
||||||
totalAdded += result.added();
|
totalAdded += result.added();
|
||||||
if (!result.errors().isEmpty()) {
|
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) {
|
} 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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user