diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java index ca2826e4..e366276c 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java @@ -49,7 +49,9 @@ public class WikiContextService { } StringBuilder sb = new StringBuilder("\n"); - sb.append("[Relevant wiki pages for this query. Use wiki_read_page(slug) for full content.]\n\n"); + sb.append("[Relevant wiki pages for this query. Use wiki_read_page(slug) for full content. " + + "When using information from these pages in your answer, always cite the source page title, " + + "e.g. 「来源:[[页面标题]]」or「(来源:页面标题)」.]\n\n"); int totalChars = 0; int maxChars = properties.getMaxContextChars(); diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java index 4341adfa..f9289edf 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java @@ -72,6 +72,8 @@ public class WikiTool { @Tool(description = """ Read a wiki page. Use maxChars to limit size (recommended: 3000-6000 for most tasks). Use sectionHeading to read only one section by its heading text. + The result includes a "sourceFiles" field listing the source documents this page was derived from. + When using content from this page in your answer, cite the page title and source files. """) public String wiki_read_page( @ToolParam(description = "Agent ID") Long agentId, @@ -167,6 +169,8 @@ public class WikiTool { @Tool(description = """ Search wiki pages. Returns snippet so you can judge relevance without reading the full page. Default topK=5 is sufficient for most queries. + Each result includes "slug" and "title" — use wiki_read_page to get full content. + When using wiki information in your answer, always cite the source page title. """) public String wiki_search_pages( @ToolParam(description = "Agent ID") Long agentId, @@ -214,8 +218,9 @@ public class WikiTool { @Tool(description = """ Chunk-level semantic search in the wiki knowledge base. - Returns raw text fragments closest to the query with similarity scores. + Returns raw text fragments closest to the query with similarity scores and source page title. Use when wiki_search_pages results are not specific enough. + When using retrieved content in your answer, cite the source page title shown in each result. """) public String wiki_semantic_search( @ToolParam(description = "Agent ID") Long agentId, diff --git a/mateclaw-ui/src/composables/useMarkdownRenderer.ts b/mateclaw-ui/src/composables/useMarkdownRenderer.ts index 64c33490..a6ce981f 100644 --- a/mateclaw-ui/src/composables/useMarkdownRenderer.ts +++ b/mateclaw-ui/src/composables/useMarkdownRenderer.ts @@ -88,7 +88,7 @@ const markedInstance = new Marked({ // 配置 DOMPurify — 允许 Markdown + 代码块复制按钮的标签和属性 const purifyConfig = { - ADD_ATTR: ['target', 'rel', 'class', 'data-code', 'data-echarts-option', 'data-wiki-title', 'type', 'viewBox', 'fill', 'stroke', 'stroke-width', 'd', 'x', 'y', 'width', 'height', 'rx', 'ry', 'points'], + ADD_ATTR: ['target', 'rel', 'class', 'data-code', 'data-echarts-option', 'data-wiki-title', 'data-slug', 'type', 'viewBox', 'fill', 'stroke', 'stroke-width', 'd', 'x', 'y', 'width', 'height', 'rx', 'ry', 'points'], ADD_TAGS: ['input', 'button', 'svg', 'path', 'rect', 'polyline', 'circle', 'line', 'span'], } diff --git a/mateclaw-ui/src/views/Wiki/components/WikiPageViewer.vue b/mateclaw-ui/src/views/Wiki/components/WikiPageViewer.vue index 5b21992c..be007ee2 100644 --- a/mateclaw-ui/src/views/Wiki/components/WikiPageViewer.vue +++ b/mateclaw-ui/src/views/Wiki/components/WikiPageViewer.vue @@ -99,9 +99,18 @@ const enrichToast = ref('') const renderedContent = computed(() => { if (!store.currentPage?.content) return '' - const content = store.currentPage.content.replace(/\[\[([^\]]+)\]\]/g, (_match, title) => { - const slug = title.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, '').replace(/\s+/g, '-') - return `${title}` + // Build a lookup map: title (normalized) → slug, for resolving [[Title]] links + const titleToSlug = new Map() + for (const p of store.pages) { + if (p.title && p.slug) { + titleToSlug.set(p.title.trim().toLowerCase(), p.slug) + } + } + const content = store.currentPage.content.replace(/\[\[([^\]]+)\]\]/g, (_match, raw) => { + const title = raw.trim() + // Prefer exact title match; fall back to slug-style guess + const slug = titleToSlug.get(title.toLowerCase()) ?? title.toLowerCase().replace(/\s+/g, '-') + return `${title}` }) return renderMarkdown(content) })