feat(wiki): hot cache storage skeleton — table, entity, value object, read service

This commit is contained in:
matevip 2026-05-02 21:45:43 +08:00
parent 47127fe918
commit a8f0998c24
7 changed files with 287 additions and 0 deletions

View File

@ -0,0 +1,87 @@
package vip.mate.wiki.hotcache;
import lombok.Builder;
import lombok.Data;
import java.time.Instant;
import java.util.List;
/**
* In-memory value object for one hot cache snapshot four sections plus
* an "updated" timestamp that ride together into the agent system prompt.
*
* <p>Wire shape on disk is the rendered markdown produced by
* {@link #toMarkdown()}; the structured form is preserved here so the
* updater LLM can return JSON-shaped sections that are validated, scored,
* and rendered uniformly across versions.
*
* <p>Example rendering:
* <pre>
* ---
* type: meta
* updated: 2026-05-02T08:30:00Z
* ---
*
* ## Last Updated
* 2026-05-02 Ingested 3 papers on RedLock; flagged contradiction with paxos-comparison page
*
* ## Key Recent Facts
* - RedLock has known safety issues under network partition (Kleppmann 2016)
* - Internal Redis 7.4 release notes confirm scheduled deprecation in 8.0
*
* ## Recent Changes
* - Created: [[redlock-safety-analysis]]
* - Updated: [[distributed-locks]], [[paxos-comparison]]
*
* ## Active Threads
* - Open question: should we recommend ZooKeeper for new services?
* </pre>
*/
@Data
@Builder
public class HotCacheContent {
private Instant updatedAt;
/** Single-paragraph "what happened most recently" headline. */
private String lastUpdatedSummary;
@Builder.Default private List<String> keyRecentFacts = List.of();
@Builder.Default private List<String> recentChanges = List.of();
@Builder.Default private List<String> activeThreads = List.of();
/**
* Renders the structured snapshot to its on-disk markdown form. Empty
* sections render as {@code (none)} so the output remains readable when
* a rebuild produces less than a full set of sections.
*/
public String toMarkdown() {
StringBuilder sb = new StringBuilder();
sb.append("---\n");
sb.append("type: meta\n");
sb.append("updated: ").append(updatedAt != null ? updatedAt.toString() : "unknown").append("\n");
sb.append("---\n\n");
sb.append("## Last Updated\n");
sb.append(lastUpdatedSummary != null && !lastUpdatedSummary.isBlank()
? lastUpdatedSummary : "(no recent activity)").append("\n\n");
appendBulletSection(sb, "Key Recent Facts", keyRecentFacts);
appendBulletSection(sb, "Recent Changes", recentChanges);
appendBulletSection(sb, "Active Threads", activeThreads);
return sb.toString();
}
private static void appendBulletSection(StringBuilder sb, String heading, List<String> bullets) {
sb.append("## ").append(heading).append('\n');
if (bullets == null || bullets.isEmpty()) {
sb.append("(none)\n\n");
return;
}
for (String bullet : bullets) {
sb.append("- ").append(bullet).append('\n');
}
sb.append('\n');
}
}

View File

@ -0,0 +1,23 @@
package vip.mate.wiki.hotcache;
/**
* Why a hot cache row was last rebuilt. Stored as
* {@link Enum#name()} on {@code mate_wiki_hot_cache.update_reason}.
*/
public enum HotCacheUpdateReason {
/** A wiki compile job for some raw material in this KB just finished. */
COMPILE_DONE,
/** A page in this KB was edited directly (not via compile). */
PAGE_UPDATED,
/** A conversation that read pages from this KB ended (debounced). */
CONVERSATION_END,
/** Operator hit the "rebuild now" button. */
MANUAL,
/** Periodic refresh fired because no event had triggered a rebuild for a while. */
STALE_CHECK,
}

View File

@ -0,0 +1,49 @@
package vip.mate.wiki.hotcache;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import vip.mate.wiki.model.WikiHotCacheEntity;
import vip.mate.wiki.repository.WikiHotCacheMapper;
import java.util.Optional;
/**
* Read/write service for KB-level hot cache snapshots.
*
* <p>This PR provides read methods only the LLM-driven rebuilder lands
* in a follow-up PR (along with the event listener and debounce window).
* Read methods are exposed now so the agent-prompt provider can wire up
* against a stable API even before any cache rows exist.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WikiHotCacheService {
private final WikiHotCacheMapper mapper;
/**
* Returns the active hot cache row for {@code kbId}, or empty when
* none has ever been generated for this KB. The mapper logical-delete
* filter excludes soft-deleted rows automatically.
*/
public Optional<WikiHotCacheEntity> findByKb(Long kbId) {
if (kbId == null) {
return Optional.empty();
}
return Optional.ofNullable(mapper.selectOne(
new LambdaQueryWrapper<WikiHotCacheEntity>()
.eq(WikiHotCacheEntity::getKbId, kbId)));
}
/**
* Returns the rendered markdown body for {@code kbId}, or {@code null}
* when no cache row exists. Convenience for the agent-prompt provider
* which only ever needs the body.
*/
public String getContentOrNull(Long kbId) {
return findByKb(kbId).map(WikiHotCacheEntity::getContent).orElse(null);
}
}

View File

@ -0,0 +1,51 @@
package vip.mate.wiki.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* One row per knowledge base the rolling "what happened recently here"
* snapshot rendered into the agent system prompt at conversation start.
*
* <p>The body is markdown produced from four sections (Last Updated, Key
* Recent Facts, Recent Changes, Active Threads) by an LLM rebuild call.
* This entity is the persistence shape; the value-object form lives in
* {@link vip.mate.wiki.hotcache.HotCacheContent}.
*/
@Data
@TableName("mate_wiki_hot_cache")
public class WikiHotCacheEntity {
@TableId(type = IdType.AUTO)
private Long id;
private Long kbId;
/** Markdown body, target ≤ 4096 chars after rendering. */
private String content;
/** SHA-256 of {@link #content}; lets the updater short-circuit identical rewrites. */
private String contentHash;
private LocalDateTime lastUpdated;
/** {@link vip.mate.wiki.hotcache.HotCacheUpdateReason#name()} for the most recent rebuild. */
private String updateReason;
private Long rebuildCount;
private LocalDateTime lastRebuildStartedAt;
private Long lastRebuildDurationMs;
private String lastRebuildError;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}

View File

@ -0,0 +1,9 @@
package vip.mate.wiki.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vip.mate.wiki.model.WikiHotCacheEntity;
@Mapper
public interface WikiHotCacheMapper extends BaseMapper<WikiHotCacheEntity> {
}

View File

@ -0,0 +1,37 @@
-- mate_wiki_hot_cache: KB-level rolling snapshot of "what happened recently"
-- in this knowledge base, injected into the agent system prompt so the model
-- doesn't have to wiki_search the obvious every turn.
--
-- One row per KB (uk_whc_kb). Body is markdown rendered from a four-section
-- structure (Last Updated / Key Recent Facts / Recent Changes / Active Threads),
-- regenerated by an LLM call after compile/page/conversation events; this
-- migration only provisions storage. Consumers and updater land in later PRs.
CREATE TABLE IF NOT EXISTS mate_wiki_hot_cache (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
kb_id BIGINT NOT NULL UNIQUE,
-- Markdown body, target ≤ 4096 chars after rendering.
content CLOB,
-- SHA-256 of `content`; lets the updater skip a write when LLM output
-- ends up identical to the previous body.
content_hash CHAR(64),
last_updated TIMESTAMP,
-- HotCacheUpdateReason enum name (COMPILE_DONE / PAGE_UPDATED /
-- CONVERSATION_END / MANUAL / STALE_CHECK).
update_reason VARCHAR(32),
rebuild_count BIGINT NOT NULL DEFAULT 0,
last_rebuild_started_at TIMESTAMP,
last_rebuild_duration_ms BIGINT,
last_rebuild_error VARCHAR(512),
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_whc_kb ON mate_wiki_hot_cache (kb_id, deleted);

View File

@ -0,0 +1,31 @@
-- mate_wiki_hot_cache: KB-level rolling snapshot of "what happened recently"
-- in this knowledge base, injected into the agent system prompt so the model
-- doesn't have to wiki_search the obvious every turn.
--
-- One row per KB (uk_whc_kb). Body is markdown rendered from a four-section
-- structure (Last Updated / Key Recent Facts / Recent Changes / Active Threads),
-- regenerated by an LLM call after compile/page/conversation events; this
-- migration only provisions storage. Consumers and updater land in later PRs.
CREATE TABLE IF NOT EXISTS mate_wiki_hot_cache (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
kb_id BIGINT NOT NULL,
content TEXT,
content_hash CHAR(64),
last_updated DATETIME(3),
update_reason VARCHAR(32),
rebuild_count BIGINT NOT NULL DEFAULT 0,
last_rebuild_started_at DATETIME(3),
last_rebuild_duration_ms BIGINT,
last_rebuild_error VARCHAR(512),
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
deleted TINYINT NOT NULL DEFAULT 0,
UNIQUE KEY uk_whc_kb (kb_id, deleted),
KEY idx_whc_kb (kb_id, deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;