mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): hot cache rebuild engine — LLM updater, scheduler, conversation-end listener
This commit is contained in:
parent
93807d85ef
commit
5e936fd50a
@ -0,0 +1,59 @@
|
||||
package vip.mate.wiki.hotcache;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.memory.event.ConversationCompletedEvent;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wires the wiki hot-cache rebuild trigger into the existing event flow.
|
||||
*
|
||||
* <p>Today only {@link ConversationCompletedEvent} is published — when a
|
||||
* chat turn ends, we schedule a rebuild for the agent's primary KB.
|
||||
* (Per the existing chat retrieval path, "primary" is the first entry
|
||||
* from {@link WikiKnowledgeBaseService#listByAgentId}, ordered by recent
|
||||
* update.)
|
||||
*
|
||||
* <p>The compile-completed and page-updated events that the design
|
||||
* envisions don't have publishers in the codebase yet; their listeners
|
||||
* will land in the PRs that introduce those publishers, to avoid
|
||||
* shipping dead glue. The scheduler exposes {@code rebuildNowBlocking}
|
||||
* so the future admin API can drive {@code MANUAL} rebuilds without
|
||||
* going through events.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HotCacheEventListener {
|
||||
|
||||
private final HotCacheUpdateScheduler scheduler;
|
||||
private final WikiKnowledgeBaseService kbService;
|
||||
|
||||
@EventListener
|
||||
public void onConversationEnd(ConversationCompletedEvent event) {
|
||||
Long agentId = event.agentId();
|
||||
if (agentId == null) return;
|
||||
Long kbId = resolvePrimaryKb(agentId);
|
||||
if (kbId == null) {
|
||||
log.debug("[HotCache] agent={} has no KB; skip post-conversation rebuild", agentId);
|
||||
return;
|
||||
}
|
||||
scheduler.scheduleRebuild(kbId, HotCacheUpdateReason.CONVERSATION_END);
|
||||
}
|
||||
|
||||
private Long resolvePrimaryKb(Long agentId) {
|
||||
try {
|
||||
List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(agentId);
|
||||
if (kbs.isEmpty()) return null;
|
||||
return kbs.get(0).getId();
|
||||
} catch (Exception e) {
|
||||
log.debug("[HotCache] KB resolution failed for agent={}: {}", agentId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package vip.mate.wiki.hotcache;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Tunables for the wiki hot-cache rebuild pipeline. Defaults match the
|
||||
* "ship cautiously" preset — short rebuild window, conservative LLM
|
||||
* timeout, modest input slice.
|
||||
*
|
||||
* <p>Override per-environment via {@code mateclaw.wiki.hot-cache.*}.
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "mateclaw.wiki.hot-cache")
|
||||
public class HotCacheProperties {
|
||||
|
||||
/** Minimum gap between two LLM-driven rebuilds for the same KB. */
|
||||
private Duration debounce = Duration.ofMinutes(5);
|
||||
|
||||
/** Time window for "recent" log/page entries fed to the LLM. */
|
||||
private Duration recentWindow = Duration.ofHours(24);
|
||||
|
||||
/** Hard cap on rendered hot cache body length (post-LLM truncation point). */
|
||||
private int maxChars = 4096;
|
||||
|
||||
/** Cap on number of recent created/updated pages fed to the prompt (each side). */
|
||||
private int maxRecentPages = 10;
|
||||
|
||||
/** Cap on chars from the previous hot cache body fed back as context. */
|
||||
private int previousContentCap = 1500;
|
||||
|
||||
/** Cap on chars from the KB activity log markdown fed to the prompt. */
|
||||
private int logExcerptCap = 2000;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package vip.mate.wiki.hotcache;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.agent.prompt.PromptLoader;
|
||||
import vip.mate.wiki.model.WikiPageEntity;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds the user-facing prompt for one hot-cache rebuild call by
|
||||
* substituting placeholders in {@code wiki/hot-cache-rebuild-user.txt}.
|
||||
*
|
||||
* <p>System prompt is loaded once and held identical across calls — the
|
||||
* intent is to keep prompt-cache reuse high once
|
||||
* {@code wiki.compile.cache.enabled} is wired in a follow-up PR.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HotCacheRebuildPromptBuilder {
|
||||
|
||||
private final HotCacheProperties props;
|
||||
|
||||
/**
|
||||
* Returns a single user-message body with the placeholders substituted.
|
||||
* Inputs are sliced to the configured caps before injection.
|
||||
*/
|
||||
public String buildUser(String previousContent,
|
||||
String logMarkdownExcerpt,
|
||||
List<WikiPageEntity> recentCreates,
|
||||
List<WikiPageEntity> recentUpdates) {
|
||||
String template = PromptLoader.loadPrompt("wiki/hot-cache-rebuild-user");
|
||||
return template
|
||||
.replace("{iso_timestamp}", Instant.now().toString())
|
||||
.replace("{recent_window}", props.getRecentWindow().toString())
|
||||
.replace("{previous_content}", abbreviate(blankToNone(previousContent), props.getPreviousContentCap()))
|
||||
.replace("{log_excerpt}", abbreviate(blankToNone(logMarkdownExcerpt), props.getLogExcerptCap()))
|
||||
.replace("{recent_creates}", renderPages(recentCreates))
|
||||
.replace("{recent_updates}", renderPages(recentUpdates));
|
||||
}
|
||||
|
||||
public String buildSystem() {
|
||||
return PromptLoader.loadPrompt("wiki/hot-cache-rebuild-system");
|
||||
}
|
||||
|
||||
private static String renderPages(List<WikiPageEntity> pages) {
|
||||
if (pages == null || pages.isEmpty()) return "(none)";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (WikiPageEntity p : pages) {
|
||||
String slug = p.getSlug() == null ? "?" : p.getSlug();
|
||||
String title = p.getTitle() == null ? slug : p.getTitle();
|
||||
sb.append("- [[").append(slug).append("]] ").append(title).append('\n');
|
||||
}
|
||||
return sb.toString().stripTrailing();
|
||||
}
|
||||
|
||||
private static String blankToNone(String s) {
|
||||
return (s == null || s.isBlank()) ? "(none)" : s;
|
||||
}
|
||||
|
||||
private static String abbreviate(String s, int max) {
|
||||
if (s == null) return "";
|
||||
if (s.length() <= max) return s;
|
||||
return s.substring(0, max - 1) + "…";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package vip.mate.wiki.hotcache;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.wiki.model.WikiHotCacheEntity;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Off-loads hot-cache rebuilds onto a virtual-thread executor and applies
|
||||
* two safety nets:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A per-KB {@link ReentrantLock} so two threads can never race on
|
||||
* the same row.</li>
|
||||
* <li>A debounce window ({@link HotCacheProperties#getDebounce()}):
|
||||
* within that window of the last rebuild start, additional events
|
||||
* are dropped because the next rebuild already covers them.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both checks are best-effort — there's no global coordination, so
|
||||
* two JVMs can each do one rebuild back-to-back. The
|
||||
* {@code last_rebuild_started_at} timestamp the updater writes is the
|
||||
* cross-process signal that PR-2's debounce uses; a future PR can lift
|
||||
* this to a distributed lock if churn shows up in metrics.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HotCacheUpdateScheduler {
|
||||
|
||||
private final HotCacheProperties props;
|
||||
private final WikiHotCacheService cacheService;
|
||||
private final WikiHotCacheUpdater updater;
|
||||
|
||||
private final ConcurrentMap<Long, ReentrantLock> locks = new ConcurrentHashMap<>();
|
||||
|
||||
/** Virtual threads — rebuilds are LLM-bound, not CPU-bound. */
|
||||
private static final ExecutorService REBUILD_EXECUTOR =
|
||||
Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
/** Schedule a rebuild for {@code kbId}; returns immediately. */
|
||||
public void scheduleRebuild(Long kbId, HotCacheUpdateReason reason) {
|
||||
if (kbId == null) return;
|
||||
REBUILD_EXECUTOR.execute(() -> attemptRebuild(kbId, reason));
|
||||
}
|
||||
|
||||
/** Synchronous variant — used by tests + the (future) admin API. */
|
||||
public void rebuildNowBlocking(Long kbId, HotCacheUpdateReason reason) {
|
||||
if (kbId == null) return;
|
||||
attemptRebuild(kbId, reason);
|
||||
}
|
||||
|
||||
private void attemptRebuild(Long kbId, HotCacheUpdateReason reason) {
|
||||
ReentrantLock lock = locks.computeIfAbsent(kbId, k -> new ReentrantLock());
|
||||
if (!lock.tryLock()) {
|
||||
log.debug("[HotCache][kb={}] rebuild already running; reason={} skipped", kbId, reason);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (withinDebounceWindow(kbId, reason)) return;
|
||||
updater.rebuild(kbId, reason);
|
||||
} catch (Exception e) {
|
||||
log.warn("[HotCache][kb={}] scheduled rebuild crashed; reason={}: {}",
|
||||
kbId, reason, e.getMessage(), e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean withinDebounceWindow(Long kbId, HotCacheUpdateReason reason) {
|
||||
// MANUAL bypasses debounce — operators triggering by hand expect to
|
||||
// see their click take effect.
|
||||
if (reason == HotCacheUpdateReason.MANUAL) return false;
|
||||
|
||||
WikiHotCacheEntity existing = cacheService.findByKb(kbId).orElse(null);
|
||||
if (existing == null || existing.getLastRebuildStartedAt() == null) return false;
|
||||
Instant lastStart = existing.getLastRebuildStartedAt()
|
||||
.atZone(ZoneId.systemDefault()).toInstant();
|
||||
if (Instant.now().isBefore(lastStart.plus(props.getDebounce()))) {
|
||||
log.debug("[HotCache][kb={}] within debounce window; reason={} skipped", kbId, reason);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,266 @@
|
||||
package vip.mate.wiki.hotcache;
|
||||
|
||||
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.prompt.Prompt;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.agent.AgentGraphBuilder;
|
||||
import vip.mate.llm.model.ModelConfigEntity;
|
||||
import vip.mate.llm.service.ModelConfigService;
|
||||
import vip.mate.system.featureflag.FeatureFlagService;
|
||||
import vip.mate.wiki.job.WikiJobStep;
|
||||
import vip.mate.wiki.job.WikiModelRoutingService;
|
||||
import vip.mate.wiki.metrics.WikiMetrics;
|
||||
import vip.mate.wiki.model.WikiHotCacheEntity;
|
||||
import vip.mate.wiki.model.WikiPageEntity;
|
||||
import vip.mate.wiki.repository.WikiHotCacheMapper;
|
||||
import vip.mate.wiki.service.WikiPageService;
|
||||
import vip.mate.wiki.service.WikiScaffoldService;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Performs one LLM-driven hot cache rebuild for a single KB. Caller
|
||||
* (typically {@link HotCacheUpdateScheduler}) handles debounce + per-KB
|
||||
* locking; this class is otherwise stateless and idempotent.
|
||||
*
|
||||
* <p>Pipeline: gather inputs (previous body + activity log excerpt +
|
||||
* recent creates / updates) → build a system+user prompt pair → resolve a
|
||||
* KB-routed chat model the same way the wiki narrative refresher does →
|
||||
* call → truncate to the configured cap → diff against the existing
|
||||
* content hash → write only if changed.
|
||||
*
|
||||
* <p>Failure modes (flag off, model resolution miss, LLM error) all
|
||||
* surface as warnings in the row's {@code last_rebuild_error}. They never
|
||||
* propagate up — the triggering event must complete regardless of cache
|
||||
* health.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WikiHotCacheUpdater {
|
||||
|
||||
private static final String FLAG = "wiki.hot_cache.enabled";
|
||||
private static final RetryTemplate NO_RETRY = RetryTemplate.builder().maxAttempts(1).build();
|
||||
|
||||
private final WikiHotCacheService cacheService;
|
||||
private final WikiHotCacheMapper mapper;
|
||||
private final HotCacheRebuildPromptBuilder promptBuilder;
|
||||
private final HotCacheProperties props;
|
||||
private final WikiModelRoutingService modelRoutingService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final AgentGraphBuilder agentGraphBuilder;
|
||||
private final FeatureFlagService featureFlagService;
|
||||
private final WikiMetrics metrics;
|
||||
private final WikiPageService pageService;
|
||||
|
||||
public WikiHotCacheUpdater(WikiHotCacheService cacheService,
|
||||
WikiHotCacheMapper mapper,
|
||||
HotCacheRebuildPromptBuilder promptBuilder,
|
||||
HotCacheProperties props,
|
||||
WikiModelRoutingService modelRoutingService,
|
||||
ModelConfigService modelConfigService,
|
||||
AgentGraphBuilder agentGraphBuilder,
|
||||
FeatureFlagService featureFlagService,
|
||||
WikiMetrics metrics,
|
||||
WikiPageService pageService) {
|
||||
this.cacheService = cacheService;
|
||||
this.mapper = mapper;
|
||||
this.promptBuilder = promptBuilder;
|
||||
this.props = props;
|
||||
this.modelRoutingService = modelRoutingService;
|
||||
this.modelConfigService = modelConfigService;
|
||||
this.agentGraphBuilder = agentGraphBuilder;
|
||||
this.featureFlagService = featureFlagService;
|
||||
this.metrics = metrics;
|
||||
this.pageService = pageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the hot cache row for {@code kbId}. Safe to call concurrently;
|
||||
* the scheduler holds the lock that makes it serial per KB.
|
||||
*/
|
||||
public void rebuild(Long kbId, HotCacheUpdateReason reason) {
|
||||
if (kbId == null) return;
|
||||
if (!featureFlagService.isEnabledForKb(FLAG, kbId)) {
|
||||
log.debug("[HotCache][kb={}] flag disabled; skip rebuild reason={}", kbId, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
Instant start = Instant.now();
|
||||
markRebuildStarted(kbId);
|
||||
|
||||
try {
|
||||
// 1. Inputs.
|
||||
String prevContent = cacheService.getContentOrNull(kbId);
|
||||
LocalDateTime since = LocalDateTime.now().minus(props.getRecentWindow());
|
||||
List<WikiPageEntity> recentCreates =
|
||||
pageService.findRecentCreated(kbId, since, props.getMaxRecentPages());
|
||||
List<WikiPageEntity> recentUpdates =
|
||||
pageService.findRecentUpdated(kbId, since, props.getMaxRecentPages());
|
||||
String logExcerpt = readLogExcerpt(kbId);
|
||||
|
||||
// 2. Cheap exit: if the KB has had no activity at all in the
|
||||
// window, skip the LLM call entirely (the snapshot would be
|
||||
// no different from what's already there).
|
||||
if (recentCreates.isEmpty() && recentUpdates.isEmpty()
|
||||
&& (logExcerpt == null || logExcerpt.isBlank())) {
|
||||
log.debug("[HotCache][kb={}] no recent activity; skip rebuild reason={}", kbId, reason);
|
||||
clearRebuildMarker(kbId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Resolve a chat model for this KB.
|
||||
ChatModel chatModel = resolveChatModel(kbId);
|
||||
if (chatModel == null) {
|
||||
log.debug("[HotCache][kb={}] no chat model resolvable; skip rebuild", kbId);
|
||||
recordError(kbId, "no chat model resolvable",
|
||||
Duration.between(start, Instant.now()).toMillis());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Build prompts + call LLM.
|
||||
String system = promptBuilder.buildSystem();
|
||||
String user = promptBuilder.buildUser(prevContent, logExcerpt, recentCreates, recentUpdates);
|
||||
Prompt prompt = new Prompt(List.of(new SystemMessage(system), new UserMessage(user)));
|
||||
String rendered = chatModel.call(prompt).getResult().getOutput().getText();
|
||||
|
||||
// 5. Truncate + persist.
|
||||
String truncated = truncate(rendered, props.getMaxChars());
|
||||
if (truncated == null || truncated.isBlank()) {
|
||||
log.warn("[HotCache][kb={}] LLM returned empty body; treating as failure", kbId);
|
||||
recordError(kbId, "LLM returned empty body",
|
||||
Duration.between(start, Instant.now()).toMillis());
|
||||
return;
|
||||
}
|
||||
String hash = sha256(truncated);
|
||||
persistRebuild(kbId, truncated, hash, reason, start);
|
||||
|
||||
metrics.recordCompileStage("hot-cache-rebuild", kbId,
|
||||
Duration.between(start, Instant.now()));
|
||||
log.info("[HotCache][kb={}] rebuilt; reason={} chars={} duration_ms={}",
|
||||
kbId, reason, truncated.length(),
|
||||
Duration.between(start, Instant.now()).toMillis());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("[HotCache][kb={}] rebuild failed; reason={}: {}",
|
||||
kbId, reason, e.getMessage(), e);
|
||||
recordError(kbId, e.getMessage(),
|
||||
Duration.between(start, Instant.now()).toMillis());
|
||||
// Intentionally swallow — caller's event must complete.
|
||||
}
|
||||
}
|
||||
|
||||
private void persistRebuild(Long kbId, String content, String hash,
|
||||
HotCacheUpdateReason reason, Instant start) {
|
||||
WikiHotCacheEntity row = cacheService.findByKb(kbId).orElseGet(() -> {
|
||||
WikiHotCacheEntity fresh = new WikiHotCacheEntity();
|
||||
fresh.setKbId(kbId);
|
||||
fresh.setRebuildCount(0L);
|
||||
return fresh;
|
||||
});
|
||||
|
||||
boolean unchanged = hash.equals(row.getContentHash());
|
||||
if (!unchanged) {
|
||||
row.setContent(content);
|
||||
row.setContentHash(hash);
|
||||
row.setRebuildCount((row.getRebuildCount() == null ? 0L : row.getRebuildCount()) + 1);
|
||||
}
|
||||
row.setLastUpdated(LocalDateTime.now());
|
||||
row.setUpdateReason(reason.name());
|
||||
row.setLastRebuildDurationMs(Duration.between(start, Instant.now()).toMillis());
|
||||
row.setLastRebuildError(null);
|
||||
|
||||
if (row.getId() == null) {
|
||||
mapper.insert(row);
|
||||
} else {
|
||||
mapper.updateById(row);
|
||||
}
|
||||
if (unchanged) {
|
||||
log.debug("[HotCache][kb={}] body unchanged; only timestamps refreshed", kbId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the most recent slice of the KB's activity log markdown page
|
||||
* (created by {@code WikiLogService.append}). Bounded to
|
||||
* {@link HotCacheProperties#getLogExcerptCap()} chars from the tail —
|
||||
* older sections are dropped. Returns null if the log page is absent.
|
||||
*/
|
||||
private String readLogExcerpt(Long kbId) {
|
||||
try {
|
||||
WikiPageEntity log = pageService.getBySlug(kbId, WikiScaffoldService.LOG_SLUG);
|
||||
if (log == null) return null;
|
||||
String content = log.getContent();
|
||||
if (content == null) return null;
|
||||
int cap = props.getLogExcerptCap();
|
||||
if (content.length() <= cap) return content;
|
||||
// Tail: latest entries are at the bottom of the markdown.
|
||||
return "…\n" + content.substring(content.length() - cap);
|
||||
} catch (Exception e) {
|
||||
log.debug("[HotCache][kb={}] log page read failed: {}", kbId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ChatModel resolveChatModel(Long kbId) {
|
||||
try {
|
||||
Long modelId = modelRoutingService.selectModelId(kbId, "hot_cache_rebuild", WikiJobStep.SUMMARY);
|
||||
ModelConfigEntity model = modelConfigService.getModel(modelId);
|
||||
if (model == null) return null;
|
||||
return agentGraphBuilder.buildRuntimeChatModel(model, NO_RETRY);
|
||||
} catch (Exception e) {
|
||||
log.debug("[HotCache][kb={}] model routing failed: {}", kbId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void markRebuildStarted(Long kbId) {
|
||||
cacheService.findByKb(kbId).ifPresent(row -> {
|
||||
row.setLastRebuildStartedAt(LocalDateTime.now());
|
||||
mapper.updateById(row);
|
||||
});
|
||||
}
|
||||
|
||||
private void clearRebuildMarker(Long kbId) {
|
||||
cacheService.findByKb(kbId).ifPresent(row -> {
|
||||
row.setLastRebuildStartedAt(null);
|
||||
mapper.updateById(row);
|
||||
});
|
||||
}
|
||||
|
||||
private void recordError(Long kbId, String error, long durationMs) {
|
||||
cacheService.findByKb(kbId).ifPresent(row -> {
|
||||
row.setLastRebuildError(error != null && error.length() > 500
|
||||
? error.substring(0, 500) : error);
|
||||
row.setLastRebuildDurationMs(durationMs);
|
||||
row.setLastRebuildStartedAt(null);
|
||||
mapper.updateById(row);
|
||||
});
|
||||
}
|
||||
|
||||
private static String truncate(String s, int max) {
|
||||
if (s == null) return "";
|
||||
if (s.length() <= max) return s;
|
||||
return s.substring(0, max - 1) + "…";
|
||||
}
|
||||
|
||||
private static String sha256(String s) {
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(s.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder(64);
|
||||
for (byte b : digest) sb.append(String.format("%02x", b));
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "no-hash";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import vip.mate.wiki.model.WikiPageEntity;
|
||||
import vip.mate.wiki.repository.WikiPageMapper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -72,6 +73,47 @@ public class WikiPageService {
|
||||
* default {@link #listByKbId} filter. Used by the admin UI's "show archived"
|
||||
* panel so users can see what they archived and recover it.
|
||||
*/
|
||||
/**
|
||||
* Pages in {@code kbId} created at or after {@code since}, newest first.
|
||||
* Used by the hot-cache rebuilder to surface "what was just added";
|
||||
* archived pages and system pages (overview/log) are excluded so the
|
||||
* snapshot stays focused on user-visible knowledge.
|
||||
*/
|
||||
public List<WikiPageEntity> findRecentCreated(Long kbId, LocalDateTime since, int limit) {
|
||||
if (kbId == null || since == null || limit <= 0) return java.util.List.of();
|
||||
List<WikiPageEntity> rows = pageMapper.selectList(
|
||||
new LambdaQueryWrapper<WikiPageEntity>()
|
||||
.eq(WikiPageEntity::getKbId, kbId)
|
||||
.ne(WikiPageEntity::getArchived, 1)
|
||||
.ne(WikiPageEntity::getPageType, WikiScaffoldService.SYSTEM_PAGE_TYPE)
|
||||
.ge(WikiPageEntity::getCreateTime, since)
|
||||
.orderByDesc(WikiPageEntity::getCreateTime)
|
||||
.last("LIMIT " + limit));
|
||||
rows.forEach(p -> p.setContent(null));
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pages in {@code kbId} updated at or after {@code since}, newest first.
|
||||
* Same exclusions as {@link #findRecentCreated}.
|
||||
*
|
||||
* <p>A row that was both created and updated in the window will appear
|
||||
* in both lists — the caller deduplicates if needed.
|
||||
*/
|
||||
public List<WikiPageEntity> findRecentUpdated(Long kbId, LocalDateTime since, int limit) {
|
||||
if (kbId == null || since == null || limit <= 0) return java.util.List.of();
|
||||
List<WikiPageEntity> rows = pageMapper.selectList(
|
||||
new LambdaQueryWrapper<WikiPageEntity>()
|
||||
.eq(WikiPageEntity::getKbId, kbId)
|
||||
.ne(WikiPageEntity::getArchived, 1)
|
||||
.ne(WikiPageEntity::getPageType, WikiScaffoldService.SYSTEM_PAGE_TYPE)
|
||||
.ge(WikiPageEntity::getUpdateTime, since)
|
||||
.orderByDesc(WikiPageEntity::getUpdateTime)
|
||||
.last("LIMIT " + limit));
|
||||
rows.forEach(p -> p.setContent(null));
|
||||
return rows;
|
||||
}
|
||||
|
||||
public List<WikiPageEntity> listArchivedByKbId(Long kbId) {
|
||||
List<WikiPageEntity> pages = pageMapper.selectList(
|
||||
new LambdaQueryWrapper<WikiPageEntity>()
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
You are the wiki hot cache rebuilder. Your job: produce a concise "what
|
||||
happened recently" snapshot for the given knowledge base, so the next AI
|
||||
session can pick up where the last one left off without re-reading every
|
||||
wiki page.
|
||||
|
||||
Output STRICT markdown with this exact structure:
|
||||
|
||||
---
|
||||
type: meta
|
||||
updated: {iso_timestamp}
|
||||
---
|
||||
|
||||
## Last Updated
|
||||
{One sentence summarizing the most recent activity, ≤ 100 chars.}
|
||||
|
||||
## Key Recent Facts
|
||||
- {Most important takeaway from recent ingest, ≤ 80 chars}
|
||||
- {Second most important, ≤ 80 chars}
|
||||
- {Third most important, ≤ 80 chars}
|
||||
|
||||
## Recent Changes
|
||||
Created: {wikilinks separated by commas, e.g. [[page-1]], [[page-2]]}
|
||||
Updated: {same shape}
|
||||
Flagged: {contradictions or ambiguous links worth attention}
|
||||
|
||||
## Active Threads
|
||||
- {Open research direction, ≤ 100 chars}
|
||||
- {Open question, ≤ 100 chars}
|
||||
|
||||
CONSTRAINTS:
|
||||
- TOTAL output (including frontmatter) MUST be ≤ 4000 characters.
|
||||
- Use ONLY information from the inputs the user provides; do NOT
|
||||
speculate, do NOT invent page slugs.
|
||||
- Sections may be empty (write "(none)" if so), but the four headers
|
||||
MUST appear, in order, exactly as shown.
|
||||
- Output ONLY the markdown above — no fences, no preamble, no
|
||||
explanation, no closing remarks.
|
||||
- Match the language of the input pages (Chinese inputs → Chinese
|
||||
snapshot).
|
||||
@ -0,0 +1,21 @@
|
||||
## Previous hot cache (for continuity; may be empty)
|
||||
|
||||
{previous_content}
|
||||
|
||||
## Recent activity log excerpt (last {recent_window})
|
||||
|
||||
{log_excerpt}
|
||||
|
||||
## Recently created pages
|
||||
|
||||
{recent_creates}
|
||||
|
||||
## Recently updated pages
|
||||
|
||||
{recent_updates}
|
||||
|
||||
---
|
||||
|
||||
Produce the rebuilt hot cache snapshot now, using the structure and
|
||||
constraints in the system prompt. Use the timestamp {iso_timestamp} in
|
||||
the frontmatter.
|
||||
Loading…
Reference in New Issue
Block a user