mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 20:34:39 +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;
|
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 ====================
|
// ==================== Transformations ====================
|
||||||
|
|
||||||
@Tool(description = """
|
@Tool(description = """
|
||||||
|
|||||||
@ -150,4 +150,83 @@ class WikiToolPermissionTest {
|
|||||||
assertTrue(out.contains("Not permitted"), out);
|
assertTrue(out.contains("Not permitted"), out);
|
||||||
verify(h.pageService(), never()).createPage(anyLong(), any(), any(), any(), any(), any());
|
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