package vip.mate.wiki.tool;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
import vip.mate.wiki.model.WikiPageEntity;
import vip.mate.wiki.model.WikiRawMaterialEntity;
import vip.mate.wiki.service.WikiKnowledgeBaseService;
import vip.mate.wiki.service.WikiPageService;
import vip.mate.wiki.service.WikiRawMaterialService;
import java.util.List;
/**
* Wiki 知识库工具
*
* 供 Agent 在对话中按需读取 Wiki 页面内容。
* kbId 通过 agentId 自动解析,LLM 无需传递。
*
* @author MateClaw Team
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WikiTool {
private final WikiPageService pageService;
private final WikiKnowledgeBaseService kbService;
private final WikiRawMaterialService rawService;
@Tool(description = """
读取 Wiki 知识库中指定页面的完整内容。
当系统提示词中的 Wiki 页面摘要不够详细时,使用此工具获取完整内容。
返回 Markdown 格式的页面内容,包含 [[双向链接]] 和来源原始文件信息。
""")
public String wiki_read_page(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "页面标识符 (slug)") String slug) {
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");
}
WikiPageEntity page = pageService.getBySlug(kbId, slug);
if (page == null) {
return error("Page not found: " + slug);
}
JSONObject result = JSONUtil.createObj()
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("version", page.getVersion())
.set("lastUpdatedBy", page.getLastUpdatedBy())
.set("content", page.getContent())
.set("sourceFiles", resolveSourceFiles(page.getSourceRawIds()));
return result.toString();
}
@Tool(description = """
列出 Wiki 知识库中的所有页面。
返回页面列表,包含标题、slug 和摘要。
""")
public String wiki_list_pages(
@ToolParam(description = "当前 Agent 的 ID") Long agentId) {
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent");
}
List pages = pageService.listSummaries(kbId);
JSONArray arr = new JSONArray();
for (WikiPageEntity page : pages) {
arr.add(JSONUtil.createObj()
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("summary", page.getSummary()));
}
return JSONUtil.createObj()
.set("kbId", kbId)
.set("pageCount", pages.size())
.set("pages", arr)
.toString();
}
@Tool(description = """
在 Wiki 知识库中搜索页面。
按关键词搜索页面标题、摘要和正文内容,返回匹配的页面列表及其来源文件。
""")
public String wiki_search_pages(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "搜索关键词") String query) {
if (query == null || query.isBlank()) {
return error("query is required");
}
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent");
}
String queryLower = query.toLowerCase();
List allPages = pageService.listByKbIdWithContent(kbId);
List matched = allPages.stream()
.filter(p -> (p.getTitle() != null && p.getTitle().toLowerCase().contains(queryLower))
|| (p.getSummary() != null && p.getSummary().toLowerCase().contains(queryLower))
|| (p.getContent() != null && p.getContent().toLowerCase().contains(queryLower)))
.limit(20)
.toList();
JSONArray arr = new JSONArray();
for (WikiPageEntity page : matched) {
JSONObject obj = JSONUtil.createObj()
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("summary", page.getSummary())
.set("sourceFiles", resolveSourceFiles(page.getSourceRawIds()));
boolean titleMatch = page.getTitle() != null && page.getTitle().toLowerCase().contains(queryLower);
boolean contentMatch = page.getContent() != null && page.getContent().toLowerCase().contains(queryLower);
obj.set("matchIn", titleMatch ? "title" : contentMatch ? "content" : "summary");
arr.add(obj);
}
return JSONUtil.createObj()
.set("kbId", kbId)
.set("query", query)
.set("matchCount", matched.size())
.set("pages", arr)
.toString();
}
@Tool(description = """
追溯 Wiki 页面的来源原始文件。
查询指定页面是由哪些原始文档生成的,返回文件名、类型、路径等信息。
用于回答"这个内容出自哪篇文档"类的问题。
""")
public String wiki_trace_source(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "页面标识符 (slug)") String slug) {
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");
}
WikiPageEntity page = pageService.getBySlug(kbId, slug);
if (page == null) {
return error("Page not found: " + slug);
}
return JSONUtil.createObj()
.set("pageTitle", page.getTitle())
.set("pageSlug", page.getSlug())
.set("sourceFiles", resolveSourceFiles(page.getSourceRawIds()))
.toString();
}
@Tool(description = """
在 Wiki 知识库中创建新页面。
用于保存任务执行结果、分析报告、会议纪要等有价值的信息。
内容使用 Markdown 格式。页面标识符 (slug) 会从标题自动生成。
""")
public String wiki_create_page(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "页面标题") String title,
@ToolParam(description = "页面内容 (Markdown 格式)") String content) {
if (title == null || title.isBlank()) {
return error("title is required");
}
if (content == null || content.isBlank()) {
return error("content is required");
}
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent. Create one first.");
}
// 从标题生成 slug
String slug = title.toLowerCase()
.replaceAll("[^a-z0-9\\u4e00-\\u9fff]+", "-")
.replaceAll("^-|-$", "");
if (slug.isBlank()) {
slug = "page-" + System.currentTimeMillis();
}
// 检查 slug 是否已存在
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
if (existing != null) {
slug = slug + "-" + System.currentTimeMillis() % 10000;
}
// 生成摘要(取前 200 字符)
String summary = content.length() > 200 ? content.substring(0, 200) + "..." : content;
WikiPageEntity page = pageService.createPage(kbId, slug, title, content, summary, null);
log.info("[WikiTool] Created page: {} (slug={}, kbId={})", title, slug, kbId);
return JSONUtil.createObj()
.set("ok", true)
.set("message", "Page created successfully")
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("kbId", kbId)
.toString();
}
/**
* 通过 agentId 自动解析关联的知识库 ID
*
* 查找逻辑:Agent 专属 KB + 公共 KB(agent_id IS NULL),取第一个。
*/
private Long resolveKbId(Long agentId) {
List kbs = kbService.listByAgentId(agentId);
if (kbs.isEmpty()) {
// agentId 为 null 时也尝试查公共 KB
kbs = kbService.listAll();
}
return kbs.isEmpty() ? null : kbs.get(0).getId();
}
/**
* 将 sourceRawIds JSON 数组解析为原始文件信息列表
*/
private JSONArray resolveSourceFiles(String sourceRawIdsJson) {
JSONArray result = new JSONArray();
if (sourceRawIdsJson == null || sourceRawIdsJson.isBlank()) return result;
try {
List rawIds = new ObjectMapper().readValue(sourceRawIdsJson, new TypeReference>() {});
for (Long rawId : rawIds) {
WikiRawMaterialEntity raw = rawService.getById(rawId);
if (raw != null) {
result.add(JSONUtil.createObj()
.set("rawId", raw.getId())
.set("title", raw.getTitle())
.set("sourceType", raw.getSourceType())
.set("sourcePath", raw.getSourcePath()));
}
}
} catch (Exception e) {
log.warn("[WikiTool] Failed to resolve source files: {}", e.getMessage());
}
return result;
}
private String error(String message) {
return JSONUtil.createObj().set("error", message).toString();
}
}