fix(wiki): eager 0-pages = partial when chunks indexed; per-KB structured route; archived filter completion

This commit is contained in:
matevip 2026-04-25 19:02:33 +08:00
parent 3f25064ef9
commit 5206d65be7
3 changed files with 66 additions and 8 deletions

View File

@ -36,4 +36,15 @@ public class WikiKbConfig {
/** Global fallback model chain for all steps in this KB */
private List<Long> fallbackModelIds;
/**
* RFC-051 PR-6b follow-up: per-KB opt-in for structured route output.
* <p>
* Different KBs run different chat models DashScope and Anthropic
* follow the format hint reliably; weaker locally-served Ollama models
* may not. Keeping the flag per-KB lets users flip it where it pays
* off. {@code null} (the common case) falls back to
* {@link vip.mate.wiki.WikiProperties#isUseStructuredRoute()}.
*/
private Boolean useStructuredRoute;
}

View File

@ -68,24 +68,28 @@ public class WikiPageService {
}
/**
* 列出知识库的所有页面不含 content
* 列出知识库的所有页面不含 content
* RFC-051 PR-7: archived 页面默认不返回
*/
public List<WikiPageEntity> listByKbId(Long kbId) {
List<WikiPageEntity> pages = pageMapper.selectList(
new LambdaQueryWrapper<WikiPageEntity>()
.eq(WikiPageEntity::getKbId, kbId)
.ne(WikiPageEntity::getArchived, 1)
.orderByAsc(WikiPageEntity::getTitle));
pages.forEach(p -> p.setContent(null));
return pages;
}
/**
* 列出知识库所有页面 content用于全文搜索
* 列出知识库所有页面 content用于全文搜索
* RFC-051 PR-7: archived 页面不参与 enrich / 全文搜索遍历
*/
public List<WikiPageEntity> listByKbIdWithContent(Long kbId) {
return pageMapper.selectList(
new LambdaQueryWrapper<WikiPageEntity>()
.eq(WikiPageEntity::getKbId, kbId)
.ne(WikiPageEntity::getArchived, 1)
.orderByAsc(WikiPageEntity::getTitle));
}
@ -226,6 +230,10 @@ public class WikiPageService {
List<WikiPageEntity> pages = pageMapper.selectList(
new LambdaQueryWrapper<WikiPageEntity>()
.eq(WikiPageEntity::getKbId, kbId)
// RFC-051 PR-7: a raw's archived pages stop showing up in the
// sidebar's "filter by raw" listing. Lineage is still queryable
// by hitting the page directly via slug.
.ne(WikiPageEntity::getArchived, 1)
.like(WikiPageEntity::getSourceRawIds, rawId.toString())
.orderByAsc(WikiPageEntity::getTitle));
pages.forEach(p -> p.setContent(null));

View File

@ -284,9 +284,25 @@ public class WikiProcessingService {
String finalStatus;
String finalDetail = null;
if (totalPages == 0) {
rawService.updateProcessingStatus(rawId, "failed", "No pages generated from LLM response");
finalStatus = "failed";
finalDetail = "No pages generated from LLM response";
// RFC-051 follow-up: previously this was an unconditional "failed".
// But chunks were already persisted (and the materials are searchable
// via wiki_semantic_search) the only thing that actually went wrong
// was the LLM not synthesizing pages. Treat that as partial when chunks
// landed: search works, the agent can still wiki_compile_page on demand,
// and the row is rerun-able. Reserve "failed" for the case where nothing
// got indexed at all.
if (totalChunks > 0) {
finalDetail = "Indexed " + totalChunks
+ " chunk(s) but no pages were generated. Search and wiki_compile_page still work; reprocess to retry page generation.";
rawService.updateProcessingStatus(rawId, "partial", finalDetail);
finalStatus = "partial";
log.info("[Wiki] Eager produced 0 pages but {} chunks indexed; marking partial for raw={}",
totalChunks, rawId);
} else {
rawService.updateProcessingStatus(rawId, "failed", "No pages generated from LLM response");
finalStatus = "failed";
finalDetail = "No pages generated from LLM response";
}
} else if (failedChunks > 0 || failedPages > 0) {
// 部分成功chunk 整体失败 chunk 内有 page 失败
// M2 v2 follow-uppage 级失败原本被计入 completed现在正确归 partial
@ -362,7 +378,10 @@ public class WikiProcessingService {
// 如果未来加了事务包裹 processRawMaterial这里的异步任务需要改用
// TransactionSynchronizationManager.registerSynchronization(afterCommit)
// 否则新线程会查不到 chunk事务未提交导致 embedding 静默跳过
if (totalPages > 0) {
// RFC-051 follow-up: trigger embedding whenever chunks landed, not only when
// pages were produced. Otherwise the partial-with-no-pages case above ends up
// with chunks in DB but never embedded, so semantic search silently misses them.
if (totalChunks > 0) {
final Long fKbId = kb.getId();
WIKI_EXECUTOR.submit(() -> {
try {
@ -667,9 +686,11 @@ public class WikiProcessingService {
.replace("{raw_content}", textContent);
// RFC-051 PR-6b: optionally inject Spring AI's structured-output hint so the
// LLM produces strict RouteResult JSON. Default off; flip via mate.wiki.use-structured-route.
// LLM produces strict RouteResult JSON. KB config wins; falls back to global
// mate.wiki.use-structured-route default when the KB hasn't expressed a preference.
boolean useStructured = resolveStructuredRouteFlag(kb);
org.springframework.ai.converter.BeanOutputConverter<vip.mate.wiki.dto.RouteResult> routeConverter =
properties.isUseStructuredRoute()
useStructured
? new org.springframework.ai.converter.BeanOutputConverter<>(vip.mate.wiki.dto.RouteResult.class)
: null;
if (routeConverter != null) {
@ -1790,6 +1811,24 @@ public class WikiProcessingService {
}
}
/**
* RFC-051 PR-6b follow-up: KB-level override for structured route output,
* falling back to the global property when the KB hasn't set a preference.
* Parse failures fall back to global too never block ingest on bad config.
*/
private boolean resolveStructuredRouteFlag(WikiKnowledgeBaseEntity kb) {
boolean fallback = properties.isUseStructuredRoute();
if (kb == null || kb.getConfigContent() == null) return fallback;
try {
WikiKbConfig config = objectMapper.readValue(kb.getConfigContent(), WikiKbConfig.class);
return config.getUseStructuredRoute() != null
? config.getUseStructuredRoute()
: fallback;
} catch (Exception e) {
return fallback;
}
}
/**
* RFC-051 PR-1b: read {@code ingestMode} from KB config JSON. Returns null
* on any parse error or missing field so the caller falls through to eager.