mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): add wiki_update_page (in-place edit) and wiki_stale_pages tools
This commit is contained in:
parent
3fe1f65bef
commit
1388b6eec8
@ -839,6 +839,90 @@ public class WikiTool {
|
||||
return "Wikilink enrichment queued for: " + slug;
|
||||
}
|
||||
|
||||
// ==================== Page update / stale review ====================
|
||||
|
||||
@Tool(description = """
|
||||
Update an existing wiki page's Markdown body IN PLACE, by slug. This
|
||||
preserves the page's identity, slug, backlinks and version history.
|
||||
Use this to revise or extend a page — do NOT delete and recreate it
|
||||
(that drops links and can leave duplicate pages behind). The summary
|
||||
is re-derived from the new content unless you pass one explicitly.
|
||||
""")
|
||||
public String wiki_update_page(
|
||||
@ToolParam(description = "Agent ID") Long agentId,
|
||||
@ToolParam(description = "Slug of the page to update (from wiki_list_pages / wiki_read_page)") String slug,
|
||||
@ToolParam(description = "New full Markdown content for the page body") String content,
|
||||
@ToolParam(description = "New one-line summary (optional; omit to auto-derive from content)", required = false) String summary,
|
||||
@ToolParam(description = "Target knowledge base name (from wiki_list_kbs). Omit to use the agent's primary KB; switch to `kbId` when two KBs share the name.", required = false) String kbName,
|
||||
@ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
|
||||
|
||||
if (slug == null || slug.isBlank()) {
|
||||
return error("slug is required");
|
||||
}
|
||||
if (content == null || content.isBlank()) {
|
||||
return error("content is required");
|
||||
}
|
||||
KbResolution kbRes = resolveKb(agentId, kbName, kbId);
|
||||
if (kbRes.hasError()) return kbRes.errorJson();
|
||||
kbId = kbRes.kbId();
|
||||
|
||||
WikiPageEntity page = pageService.getBySlug(kbId, slug);
|
||||
if (page == null) {
|
||||
return error("Page not found: '" + slug + "'. Use wiki_list_pages to find the right slug.");
|
||||
}
|
||||
String writeErr = checkWrite(agentId, kbId, page.getPageType(),
|
||||
WikiPageTypePermissionService.WriteOp.UPDATE);
|
||||
if (writeErr != null) return writeErr;
|
||||
|
||||
// summary == null → service re-derives it from the new content.
|
||||
WikiPageEntity updated = pageService.updatePageManually(
|
||||
kbId, slug, content, (summary == null || summary.isBlank()) ? null : summary);
|
||||
return JSONUtil.createObj()
|
||||
.set("ok", true)
|
||||
.set("slug", updated.getSlug())
|
||||
.set("title", updated.getTitle())
|
||||
.set("version", updated.getVersion())
|
||||
.set("message", "Page updated in place (slug and backlinks preserved).")
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Tool(description = """
|
||||
List wiki pages currently marked STALE (needing review) in a knowledge
|
||||
base. A page goes stale when a fact page it depends on was updated, so
|
||||
its synthesis/analysis content may now be out of date. Returns each
|
||||
stale page's title, slug, pageType, knowledge layer and the reason it
|
||||
was flagged. Use this to find what to re-check or re-summarize before
|
||||
relying on experience/analysis pages.
|
||||
""")
|
||||
public String wiki_stale_pages(
|
||||
@ToolParam(description = "Agent ID") Long agentId,
|
||||
@ToolParam(description = "Target knowledge base name (from wiki_list_kbs). Omit to use the agent's primary KB; switch to `kbId` when two KBs share the name.", required = false) String kbName,
|
||||
@ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
|
||||
|
||||
KbResolution kbRes = resolveKb(agentId, kbName, kbId);
|
||||
if (kbRes.hasError()) return kbRes.errorJson();
|
||||
kbId = kbRes.kbId();
|
||||
|
||||
WikiPageTypePermissionService.Access access = pageTypeAccess(agentId, kbId);
|
||||
JSONArray arr = new JSONArray();
|
||||
for (WikiPageEntity p : pageService.listByKbId(kbId)) {
|
||||
if (p.getStale() == null || p.getStale() == 0) continue;
|
||||
if (p.getArchived() != null && p.getArchived() == 1) continue;
|
||||
if (!canRead(access, p)) continue; // honour pageType read permissions
|
||||
arr.add(JSONUtil.createObj()
|
||||
.set("slug", p.getSlug())
|
||||
.set("title", p.getTitle())
|
||||
.set("pageType", p.getPageType())
|
||||
.set("knowledgeLayer", p.getKnowledgeLayer())
|
||||
.set("staleReason", p.getStaleReasonJson() == null ? "" : p.getStaleReasonJson()));
|
||||
}
|
||||
return JSONUtil.createObj()
|
||||
.set("kbId", String.valueOf(kbId))
|
||||
.set("staleCount", arr.size())
|
||||
.set("pages", arr)
|
||||
.toString();
|
||||
}
|
||||
|
||||
// ==================== Transformations ====================
|
||||
|
||||
@Tool(description = """
|
||||
|
||||
@ -150,4 +150,83 @@ class WikiToolPermissionTest {
|
||||
assertTrue(out.contains("Not permitted"), out);
|
||||
verify(h.pageService(), never()).createPage(anyLong(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
// ---------- wiki_update_page: in-place update, no delete/recreate ----------
|
||||
|
||||
@Test
|
||||
void updatePage_allowed_updatesInPlace() {
|
||||
Harness h = harness(List.of(row("concept", 1, 1, 1, 1, "allow")));
|
||||
when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept"));
|
||||
WikiPageEntity updated = page("p", "concept");
|
||||
updated.setVersion(2);
|
||||
when(h.pageService().updatePageManually(eq(KB), eq("p"), any(), any())).thenReturn(updated);
|
||||
|
||||
String out = h.tool().wiki_update_page(AGENT, "p", "new body", null, null, KB);
|
||||
|
||||
assertTrue(out.contains("\"ok\":true"), out);
|
||||
assertTrue(out.contains("updated in place"), out);
|
||||
verify(h.pageService(), times(1)).updatePageManually(eq(KB), eq("p"), eq("new body"), any());
|
||||
// crucially, it must NOT delete or recreate (the duplicate-page bug)
|
||||
verify(h.pageService(), never()).delete(anyLong(), any());
|
||||
verify(h.pageService(), never()).createPage(anyLong(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePage_updateDenied_doesNotUpdate() {
|
||||
// can read + create, but update flag off → DENY
|
||||
Harness h = harness(List.of(row("concept", 1, 1, 0, 0, "allow")));
|
||||
when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept"));
|
||||
|
||||
String out = h.tool().wiki_update_page(AGENT, "p", "new body", null, null, KB);
|
||||
|
||||
assertTrue(out.contains("Not permitted"), out);
|
||||
verify(h.pageService(), never()).updatePageManually(anyLong(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePage_missingPage_reportsNotFound() {
|
||||
Harness h = harness(List.of(row("*", 1, 1, 1, 1, "allow")));
|
||||
when(h.pageService().getBySlug(KB, "ghost")).thenReturn(null);
|
||||
|
||||
String out = h.tool().wiki_update_page(AGENT, "ghost", "body", null, null, KB);
|
||||
|
||||
assertTrue(out.contains("Page not found"), out);
|
||||
verify(h.pageService(), never()).updatePageManually(anyLong(), any(), any(), any());
|
||||
}
|
||||
|
||||
// ---------- wiki_stale_pages: lists stale, honours read filter ----------
|
||||
|
||||
private WikiPageEntity stalePage(String slug, String type, String reason) {
|
||||
WikiPageEntity p = page(slug, type);
|
||||
p.setStale(1);
|
||||
p.setStaleReasonJson(reason);
|
||||
return p;
|
||||
}
|
||||
|
||||
@Test
|
||||
void stalePages_listsOnlyStaleReadablePages() {
|
||||
// wildcard allows reading 'concept' but a specific 'secret' row denies read
|
||||
Harness h = harness(List.of(row("*", 1, 0, 0, 0, "deny"), row("secret", 0, 0, 0, 0, "deny")));
|
||||
WikiPageEntity fresh = page("fresh", "concept"); // not stale → excluded
|
||||
WikiPageEntity staleOk = stalePage("aged", "concept", "{\"reason\":\"fact updated\"}");
|
||||
WikiPageEntity staleHidden = stalePage("classified", "secret", "{\"reason\":\"x\"}");
|
||||
when(h.pageService().listByKbId(KB)).thenReturn(List.of(fresh, staleOk, staleHidden));
|
||||
|
||||
String out = h.tool().wiki_stale_pages(AGENT, null, KB);
|
||||
|
||||
assertTrue(out.contains("\"staleCount\":1"), out);
|
||||
assertTrue(out.contains("aged"), out);
|
||||
assertFalse(out.contains("classified"), out); // unreadable type filtered out
|
||||
assertFalse(out.contains("fresh"), out); // non-stale excluded
|
||||
}
|
||||
|
||||
@Test
|
||||
void stalePages_noneStale_returnsZero() {
|
||||
Harness h = harness(List.of());
|
||||
when(h.pageService().listByKbId(KB)).thenReturn(List.of(page("a", "concept"), page("b", "episode")));
|
||||
|
||||
String out = h.tool().wiki_stale_pages(AGENT, null, KB);
|
||||
|
||||
assertTrue(out.contains("\"staleCount\":0"), out);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user