mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): validate and persist structured page metadata on ingest
This commit is contained in:
parent
b50e384e0d
commit
eb63ce4865
@ -349,6 +349,25 @@ public class WikiPageService {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply schema-validated structured metadata to an existing page via a
|
||||
* partial column update — only the metadata columns are written, so this
|
||||
* never disturbs content / summary / links set by the ingest pipeline.
|
||||
* Null arguments are written as-is (e.g. clearing a prior validation set).
|
||||
*/
|
||||
public void applyMetadata(Long pageId, String metadataJson, String validationStatus,
|
||||
String validationJson, Integer profileVersion) {
|
||||
if (pageId == null) {
|
||||
return;
|
||||
}
|
||||
pageMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WikiPageEntity>()
|
||||
.eq(WikiPageEntity::getId, pageId)
|
||||
.set(WikiPageEntity::getMetadataJson, metadataJson)
|
||||
.set(WikiPageEntity::getMetadataValidationStatus, validationStatus)
|
||||
.set(WikiPageEntity::getMetadataValidationJson, validationJson)
|
||||
.set(WikiPageEntity::getProfileVersion, profileVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* List pages derived from a specific raw material (for UI sidebar filtering).
|
||||
* Uses a LIKE search on sourceRawIds JSON field — cheap and dialect-agnostic.
|
||||
|
||||
@ -70,6 +70,10 @@ public class WikiProcessingService {
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||
private vip.mate.wiki.profile.WikiPageTypeProfileService pageTypeProfileService;
|
||||
|
||||
/** Optional metadata validator, paired with {@link #pageTypeProfileService}. */
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||
private vip.mate.wiki.profile.WikiMetadataValidator metadataValidator;
|
||||
|
||||
/**
|
||||
* Read-the-failover-chain handle. Optional so the existing constructors and
|
||||
* lazy-mode tests don't have to thread a new dependency. When null, the
|
||||
@ -1156,6 +1160,12 @@ public class WikiProcessingService {
|
||||
String content = pageJson.path("content").asText("");
|
||||
String pageSummary = pageJson.path("summary").asText("");
|
||||
String pageType = pageJson.path("page_type").asText("");
|
||||
// Downgrade an unrecognised type to the profile fallback so the
|
||||
// stored page_type always belongs to the KB's profile.
|
||||
if (pageTypeProfileService != null && !pageType.isBlank()) {
|
||||
pageType = pageTypeProfileService.normalizePageType(kbId, pageType);
|
||||
}
|
||||
JsonNode metadataNode = pageJson.path("metadata");
|
||||
if (content.isBlank()) {
|
||||
log.info("[Wiki] BatchCreate: blank content for slug='{}', retrying individually", slug);
|
||||
final String blankSlug = slug;
|
||||
@ -1184,7 +1194,7 @@ public class WikiProcessingService {
|
||||
boolean wasCreated = false;
|
||||
boolean ok = false;
|
||||
try {
|
||||
wasCreated = savePageContent(kb, raw, slug, title, content, pageSummary, pageType);
|
||||
wasCreated = savePageContent(kb, raw, slug, title, content, pageSummary, pageType, metadataNode);
|
||||
if (wasCreated) {
|
||||
created.incrementAndGet();
|
||||
totalCreated++;
|
||||
@ -1376,12 +1386,18 @@ public class WikiProcessingService {
|
||||
*/
|
||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||
String slug, String title, String content, String pageSummary) {
|
||||
return savePageContent(kb, raw, slug, title, content, pageSummary, null);
|
||||
return savePageContent(kb, raw, slug, title, content, pageSummary, null, null);
|
||||
}
|
||||
|
||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||
String slug, String title, String content, String pageSummary,
|
||||
String pageType) {
|
||||
return savePageContent(kb, raw, slug, title, content, pageSummary, pageType, null);
|
||||
}
|
||||
|
||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||
String slug, String title, String content, String pageSummary,
|
||||
String pageType, JsonNode metadataNode) {
|
||||
Long kbId = kb.getId();
|
||||
Long rawId = raw.getId();
|
||||
|
||||
@ -1434,6 +1450,7 @@ public class WikiProcessingService {
|
||||
String sourceRawIds = "[" + rawId + "]";
|
||||
try {
|
||||
WikiPageEntity created = pageService.createPage(kbId, slug, title, content, pageSummary, sourceRawIds, pageType);
|
||||
applyValidatedMetadata(created, kbId, pageType, metadataNode);
|
||||
pageService.mergeSourceLineage(created.getId(), rawId, raw.getTitle());
|
||||
log.info("[Wiki] Phase B create page slug='{}' done (created)", slug);
|
||||
citationService.buildCitationsAsync(created.getId(), kbId);
|
||||
@ -1446,6 +1463,38 @@ public class WikiProcessingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the LLM-supplied metadata for a freshly created page against the
|
||||
* KB profile's pageType schema and persist the cleaned result plus its
|
||||
* validation outcome. No-op when the profile/validator beans are absent or
|
||||
* no metadata was supplied — so default-profile KBs are unaffected.
|
||||
*/
|
||||
private void applyValidatedMetadata(WikiPageEntity created, Long kbId, String pageType,
|
||||
JsonNode metadataNode) {
|
||||
if (created == null || pageTypeProfileService == null || metadataValidator == null) {
|
||||
return;
|
||||
}
|
||||
if (metadataNode == null || metadataNode.isMissingNode() || metadataNode.isNull()
|
||||
|| !metadataNode.isObject() || metadataNode.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
vip.mate.wiki.profile.WikiPageTypeProfile profile = pageTypeProfileService.resolveProfile(kbId);
|
||||
vip.mate.wiki.profile.WikiPageTypeDef def = profile.get(pageType);
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Map<String, Object> raw = objectMapper.convertValue(metadataNode, java.util.Map.class);
|
||||
vip.mate.wiki.profile.WikiMetadataValidator.ValidationResult result =
|
||||
metadataValidator.validate(def, raw, profile.isAllowAdditionalFields(), "create");
|
||||
String metadataJson = objectMapper.writeValueAsString(result.getCleaned());
|
||||
String validationJson = result.getWarnings().isEmpty()
|
||||
? null : objectMapper.writeValueAsString(result.getWarnings());
|
||||
pageService.applyMetadata(created.getId(), metadataJson, result.getStatus(),
|
||||
validationJson, profile.getVersion());
|
||||
} catch (Exception e) {
|
||||
log.warn("[Wiki] metadata validation failed for page {}: {}", created.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-012 M2 v2 — 阶段 B 单页 merge:把 chunk 文本合并进一个已有页面。
|
||||
* <p>
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
package vip.mate.wiki.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import vip.mate.wiki.model.WikiPageEntity;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* Verifies the structured-metadata persistence path against H2: applyMetadata
|
||||
* writes the metadata columns without disturbing the page content (partial
|
||||
* column update).
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"spring.flyway.enabled=true",
|
||||
"spring.flyway.locations=classpath:db/migration/h2",
|
||||
"mateclaw.feature-flag.refresh-ms=999999"
|
||||
}
|
||||
)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
class WikiPageMetadataE2ETest {
|
||||
|
||||
@Autowired
|
||||
private WikiPageService pageService;
|
||||
|
||||
@Test
|
||||
void applyMetadata_persistsColumns_withoutTouchingContent() {
|
||||
long kb = 6001L;
|
||||
WikiPageEntity page = pageService.createPage(kb, "episode-x", "Episode X",
|
||||
"## Body\n\noriginal content", "summary", "[1]", "episode");
|
||||
assertNotNull(page.getId());
|
||||
|
||||
pageService.applyMetadata(page.getId(),
|
||||
"{\"event_date\":\"2024-09-18\"}", "ok", null, 3);
|
||||
|
||||
WikiPageEntity loaded = pageService.getBySlug(kb, "episode-x");
|
||||
assertEquals("{\"event_date\":\"2024-09-18\"}", loaded.getMetadataJson());
|
||||
assertEquals("ok", loaded.getMetadataValidationStatus());
|
||||
assertEquals(3, loaded.getProfileVersion());
|
||||
// Partial update must not have wiped content / summary.
|
||||
assertEquals("## Body\n\noriginal content", loaded.getContent());
|
||||
assertEquals("summary", loaded.getSummary());
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyMetadata_warningStatusAndJson() {
|
||||
long kb = 6002L;
|
||||
WikiPageEntity page = pageService.createPage(kb, "episode-y", "Episode Y",
|
||||
"body", "summary", "[1]", "episode");
|
||||
|
||||
pageService.applyMetadata(page.getId(),
|
||||
"{\"event_date\":\"bad\"}", "warning",
|
||||
"[{\"field\":\"event_date\",\"reason\":\"expected ISO date YYYY-MM-DD\"}]", 1);
|
||||
|
||||
WikiPageEntity loaded = pageService.getBySlug(kb, "episode-y");
|
||||
assertEquals("warning", loaded.getMetadataValidationStatus());
|
||||
assertNotNull(loaded.getMetadataValidationJson());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user