feat(wiki): PR-7 archived soft-archive

This commit is contained in:
matevip 2026-04-25 19:02:32 +08:00
parent 850d59c04f
commit 1e62dbad47
6 changed files with 94 additions and 7 deletions

View File

@ -65,6 +65,14 @@ public class WikiPageEntity {
*/
private Integer locked;
/**
* RFC-051 PR-7: soft-archive flag. {@code archived=1} hides the page from
* default list / search / related results without destroying it. Used to
* tuck away pages that are no longer relevant but whose history (citations,
* source-raw lineage) should stay queryable.
*/
private Integer archived;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

View File

@ -22,9 +22,9 @@ public interface WikiPageMapper extends BaseMapper<WikiPageEntity> {
* DB keyword search (H2 + MySQL compatible LIKE).
* Does not SELECT content CLOB to avoid loading large blobs into Java heap.
*/
@Select("SELECT id, kb_id, slug, title, summary, source_raw_ids, last_updated_by " +
@Select("SELECT id, kb_id, slug, title, summary, source_raw_ids, last_updated_by, page_type " +
"FROM mate_wiki_page " +
"WHERE kb_id = #{kbId} AND deleted = 0 " +
"WHERE kb_id = #{kbId} AND deleted = 0 AND archived = 0 " +
"AND (LOWER(title) LIKE #{pattern} OR LOWER(summary) LIKE #{pattern} " +
" OR LOWER(content) LIKE #{pattern}) " +
"ORDER BY title LIMIT 20")
@ -37,14 +37,14 @@ public interface WikiPageMapper extends BaseMapper<WikiPageEntity> {
*/
@Select("<script>SELECT id, slug, title, summary, page_type AS pageType FROM mate_wiki_page " +
"WHERE id IN <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"AND deleted = 0</script>")
"AND deleted = 0 AND archived = 0</script>")
List<WikiPageLite> selectBatchLite(@Param("ids") Collection<Long> ids);
/**
* List all pages as lightweight projections (no content).
*/
@Select("SELECT id, slug, title, summary, page_type AS pageType FROM mate_wiki_page " +
"WHERE kb_id = #{kbId} AND deleted = 0 ORDER BY update_time DESC")
"WHERE kb_id = #{kbId} AND deleted = 0 AND archived = 0 ORDER BY update_time DESC")
List<WikiPageLite> selectAllLite(@Param("kbId") Long kbId);
/**
@ -59,7 +59,7 @@ public interface WikiPageMapper extends BaseMapper<WikiPageEntity> {
* Phase 1 (fast): search only title + summary columns.
*/
@Select("SELECT id FROM mate_wiki_page " +
"WHERE kb_id = #{kbId} AND deleted = 0 " +
"WHERE kb_id = #{kbId} AND deleted = 0 AND archived = 0 " +
"AND (LOWER(title) LIKE #{kw} OR LOWER(summary) LIKE #{kw}) " +
"LIMIT #{limit}")
List<Long> searchFastIds(@Param("kbId") Long kbId,
@ -70,7 +70,7 @@ public interface WikiPageMapper extends BaseMapper<WikiPageEntity> {
* Phase 2 (slow): search full content, excluding already-found IDs.
*/
@Select("<script>SELECT id FROM mate_wiki_page " +
"WHERE kb_id = #{kbId} AND deleted = 0 " +
"WHERE kb_id = #{kbId} AND deleted = 0 AND archived = 0 " +
"AND LOWER(content) LIKE #{kw} " +
"<if test='excludeIds != null and !excludeIds.isEmpty()'>" +
"AND id NOT IN <foreach collection='excludeIds' item='id' open='(' separator=',' close=')'>#{id}</foreach>" +

View File

@ -98,11 +98,15 @@ public class WikiPageService {
if (cached != null && !cached.isExpired()) {
return cached.data;
}
// RFC-051 PR-7: archived pages are hidden from default summary listings;
// PR-2 added page_type so callers can filter system pages too.
List<WikiPageEntity> pages = pageMapper.selectList(
new LambdaQueryWrapper<WikiPageEntity>()
.select(WikiPageEntity::getSlug, WikiPageEntity::getTitle,
WikiPageEntity::getSummary, WikiPageEntity::getLastUpdatedBy)
WikiPageEntity::getSummary, WikiPageEntity::getLastUpdatedBy,
WikiPageEntity::getPageType)
.eq(WikiPageEntity::getKbId, kbId)
.ne(WikiPageEntity::getArchived, 1)
.orderByAsc(WikiPageEntity::getTitle));
summaryCache.put(kbId, new CachedSummaries(pages, System.currentTimeMillis() + SUMMARY_CACHE_TTL_MS));
return pages;
@ -405,6 +409,33 @@ public class WikiPageService {
evictSummaryCache(kbId);
}
/**
* RFC-051 PR-7: flip the {@code archived} flag.
* <p>
* Archive hides the page from default list/search/related results without
* destroying it. Citation lineage and source-raw links survive, so an
* archived page can still be unarchived later or audited from raw history.
* Refuses to archive a system page since those are part of the KB's spine.
*
* @param archive true to archive, false to unarchive
* @return true on a state change, false if no-op (page missing or already in target state)
*/
@Transactional
public boolean setArchived(Long kbId, String slug, boolean archive) {
WikiPageEntity existing = getBySlug(kbId, slug);
if (existing == null) return false;
if ("system".equals(existing.getPageType())) {
log.warn("[Wiki] Refusing to archive system page kbId={}, slug={}", kbId, slug);
return false;
}
int target = archive ? 1 : 0;
if (existing.getArchived() != null && existing.getArchived() == target) return false;
existing.setArchived(target);
pageMapper.updateById(existing);
evictSummaryCache(kbId);
return true;
}
/**
* 批量删除页面 slug 列表
*/

View File

@ -450,6 +450,46 @@ public class WikiTool {
.toString();
}
@Tool(description = """
Archive a wiki page so it stops showing up in list / search / related
results, without destroying it. Use this when a page is no longer
relevant but its history (citations, raw lineage) should stay queryable.
System pages (overview / log) cannot be archived.
""")
public String wiki_archive_page(
@ToolParam(description = "Agent ID") Long agentId,
@ToolParam(description = "Page slug to archive") String slug) {
return setArchivedTool(agentId, slug, true, "archived");
}
@Tool(description = """
Unarchive a previously archived wiki page so it shows up in default
list / search / related results again. No-op when the page wasn't archived.
""")
public String wiki_unarchive_page(
@ToolParam(description = "Agent ID") Long agentId,
@ToolParam(description = "Page slug to unarchive") String slug) {
return setArchivedTool(agentId, slug, false, "unarchived");
}
private String setArchivedTool(Long agentId, String slug, boolean archive, String verb) {
if (slug == null || slug.isBlank()) return error("slug is required");
Long kbId = resolveKbId(agentId);
if (kbId == null) return error("No wiki knowledge base found for this agent");
boolean changed;
try {
changed = pageService.setArchived(kbId, slug, archive);
} catch (Exception e) {
return error(verb + " failed: " + e.getMessage());
}
return JSONUtil.createObj()
.set("ok", true)
.set("slug", slug)
.set("changed", changed)
.set("message", changed ? "Page " + verb : "Page already in that state (or not found)")
.toString();
}
@Tool(description = """
Delete an AI-generated wiki page. Cannot delete manually curated pages.
""")

View File

@ -0,0 +1,4 @@
-- V41: RFC-051 PR-7 — soft-archive flag.
-- archived=1 hides a page from list / search / related results without
-- destroying it, so re-ingesting the source raw doesn't regenerate it.
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS archived TINYINT NOT NULL DEFAULT 0;

View File

@ -0,0 +1,4 @@
-- V41: RFC-051 PR-7 — soft-archive flag.
SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page' AND COLUMN_NAME = 'archived');
SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_page ADD COLUMN archived TINYINT NOT NULL DEFAULT 0', 'SELECT 1');
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;