mateclaw/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiLinkEnrichmentService.java
matevip 3d213eb281 chore: sync multiple commits from private dev
Covers 15 upstream commits (private mirror → public):

Multi-provider failover (RFC-009):
- PR-0: extract ChatModelBuilder strategy seam
- PR-1a: AvailableProviderPool data structure
- PR-1b: startup provider liveness probe + 4 protocol strategies
- PR-1c: wire AvailableProviderPool into runtime chat-model selection
- PR-1d: provider pool REST endpoint + UI badges
- PR-1e: manual reprobe trigger + auto-reprobe on provider config change
- PR-3: per-agent provider preferences (agents can override the
  org-wide fallback chain)

Wiki subsystem (RFC-029~033):
- Relation model, resilient background jobs, light-weight processing
  path, retrieval enhancement, frontend redesign (single landing commit)
- Follow-up fixes: null guards + stats query + i18n polish, move
  WikiProcessingJobMapper to repository/ for @MapperScan, align
  implementation with RFC-029~031 spec
- Copy pass: replace "富化 / enrich" wording with clearer "链接 / link"
- Style: switch enrich/repair buttons to @element-plus/icons-vue
2026-04-19 18:37:44 +08:00

121 lines
4.6 KiB
Java

package vip.mate.wiki.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Service;
import vip.mate.wiki.WikiProperties;
import vip.mate.wiki.job.WikiModelRoutingService;
import vip.mate.wiki.model.WikiPageEntity;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
/**
* RFC-031: Lightweight wikilink enrichment service.
* Adds [[slug]] cross-references to page content without modifying
* the actual text. Corresponds to llm_wiki's enrich-wikilinks.ts.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WikiLinkEnrichmentService {
private final WikiPageService pageService;
private final WikiModelRoutingService routingService;
private final WikiProperties wikiProperties;
private static final ExecutorService WIKI_EXECUTOR =
Executors.newVirtualThreadPerTaskExecutor();
/**
* Enrich a single page with [[wikilinks]].
*/
public void enrichPage(Long pageId, Long modelId) {
WikiPageEntity page = pageService.getById(pageId);
if (page == null || page.getContent() == null) return;
String index = buildIndexPrompt(page.getKbId());
ChatModel chatModel = routingService.buildChatModel(modelId);
String enriched = callEnrichLlm(chatModel, page.getContent(), index);
if (enriched != null
&& enriched.length() >= page.getContent().length() * wikiProperties.getWikilinkMinContentRatio()) {
page.setContent(enriched);
page.setOutgoingLinks(pageService.extractLinksAsJson(enriched));
pageService.updateById(page);
}
}
/**
* Batch-enrich all pages in a KB (e.g. after initial ingest).
*/
public void enrichAllPages(Long kbId, Long modelId) {
List<WikiPageEntity> pages = pageService.listByKbIdWithContent(kbId);
String index = buildIndexPrompt(kbId);
Semaphore sem = new Semaphore(wikiProperties.getMaxParallelPhaseBPages());
for (WikiPageEntity page : pages) {
sem.acquireUninterruptibly();
WIKI_EXECUTOR.submit(() -> {
try {
enrichPageWithIndex(page, modelId, index);
} finally {
sem.release();
}
});
}
}
private void enrichPageWithIndex(WikiPageEntity page, Long modelId, String index) {
ChatModel chatModel = routingService.buildChatModel(modelId);
String enriched = callEnrichLlm(chatModel, page.getContent(), index);
if (enriched != null
&& enriched.length() >= page.getContent().length() * wikiProperties.getWikilinkMinContentRatio()) {
page.setContent(enriched);
page.setOutgoingLinks(pageService.extractLinksAsJson(enriched));
pageService.updateById(page);
}
}
private String callEnrichLlm(ChatModel model, String content, String index) {
String systemPrompt = """
You are a wiki cross-referencing assistant.
Your ONLY job: add [[slug]] markers around entity and concept names that appear in the wiki index.
Rules:
- Do NOT change any content, rewrite sentences, or add new text.
- Do NOT modify YAML frontmatter.
- Only wrap existing words/phrases with [[ and ]].
- Use the exact slug from the wiki index (not the title).
- Return the COMPLETE page text with [[wikilinks]] added.
""";
String userPrompt = "Wiki Index (slug → title):\n" + index + "\n\nPage content:\n" + content;
try {
ChatResponse response = model.call(
new Prompt(List.of(
new SystemMessage(systemPrompt),
new UserMessage(userPrompt)
))
);
return response.getResult().getOutput().getText();
} catch (Exception e) {
log.warn("[WikiEnrich] Failed to enrich page: {}", e.getMessage());
return null;
}
}
private String buildIndexPrompt(Long kbId) {
return pageService.listSummaries(kbId).stream()
.map(p -> p.getSlug() + "" + p.getTitle())
.collect(Collectors.joining("\n"));
}
}