From a8f0998c24d408d48b3d3a07c1188ad7bcc56482 Mon Sep 17 00:00:00 2001 From: matevip Date: Sat, 2 May 2026 21:45:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(wiki):=20hot=20cache=20storage=20skeleton?= =?UTF-8?q?=20=E2=80=94=20table,=20entity,=20value=20object,=20read=20serv?= =?UTF-8?q?ice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mate/wiki/hotcache/HotCacheContent.java | 87 +++++++++++++++++++ .../wiki/hotcache/HotCacheUpdateReason.java | 23 +++++ .../wiki/hotcache/WikiHotCacheService.java | 49 +++++++++++ .../mate/wiki/model/WikiHotCacheEntity.java | 51 +++++++++++ .../wiki/repository/WikiHotCacheMapper.java | 9 ++ .../db/migration/h2/V82__wiki_hot_cache.sql | 37 ++++++++ .../migration/mysql/V82__wiki_hot_cache.sql | 31 +++++++ 7 files changed, 287 insertions(+) create mode 100644 mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheContent.java create mode 100644 mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java create mode 100644 mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java create mode 100644 mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java create mode 100644 mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java create mode 100644 mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheContent.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheContent.java new file mode 100644 index 00000000..b17cc8ce --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheContent.java @@ -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. + * + *

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. + * + *

Example rendering: + *

+ * ---
+ * 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?
+ * 
+ */ +@Data +@Builder +public class HotCacheContent { + + private Instant updatedAt; + + /** Single-paragraph "what happened most recently" headline. */ + private String lastUpdatedSummary; + + @Builder.Default private List keyRecentFacts = List.of(); + @Builder.Default private List recentChanges = List.of(); + @Builder.Default private List 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 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'); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java new file mode 100644 index 00000000..00de575d --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/HotCacheUpdateReason.java @@ -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, +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java new file mode 100644 index 00000000..89e0fc53 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/hotcache/WikiHotCacheService.java @@ -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. + * + *

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 findByKb(Long kbId) { + if (kbId == null) { + return Optional.empty(); + } + return Optional.ofNullable(mapper.selectOne( + new LambdaQueryWrapper() + .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); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java b/mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java new file mode 100644 index 00000000..d3ff6b3a --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/model/WikiHotCacheEntity.java @@ -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. + * + *

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; +} diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java b/mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java new file mode 100644 index 00000000..276ae0d9 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/wiki/repository/WikiHotCacheMapper.java @@ -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 { +} diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql b/mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql new file mode 100644 index 00000000..e85ce2cc --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V82__wiki_hot_cache.sql @@ -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); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql new file mode 100644 index 00000000..6e8776b7 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V82__wiki_hot_cache.sql @@ -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;