mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat(knowledge): document link supports jumping to view
This commit is contained in:
@@ -44,6 +44,19 @@ class MemoryGetTool(BaseTool):
|
||||
"""
|
||||
super().__init__()
|
||||
self.memory_manager = memory_manager
|
||||
|
||||
from config import conf
|
||||
if conf().get("knowledge", True):
|
||||
self.description = (
|
||||
"Read specific content from memory or knowledge files. "
|
||||
"Use this to get full context from a memory file, knowledge page, or specific line range."
|
||||
)
|
||||
self.params = {**self.params}
|
||||
self.params["properties"] = {**self.params["properties"]}
|
||||
self.params["properties"]["path"] = {
|
||||
"type": "string",
|
||||
"description": "Relative path to the memory or knowledge file (e.g. 'MEMORY.md', 'memory/2026-01-01.md', 'knowledge/concepts/moe.md')"
|
||||
}
|
||||
|
||||
def execute(self, args: dict):
|
||||
"""
|
||||
@@ -68,8 +81,8 @@ class MemoryGetTool(BaseTool):
|
||||
workspace_dir = self.memory_manager.config.get_workspace()
|
||||
|
||||
# Auto-prepend memory/ if not present and not absolute path
|
||||
# Exception: MEMORY.md is in the root directory
|
||||
if not path.startswith('memory/') and not path.startswith('/') and path != 'MEMORY.md':
|
||||
# Exceptions: MEMORY.md in root, knowledge/ files at workspace root
|
||||
if not path.startswith('memory/') and not path.startswith('knowledge/') and not path.startswith('/') and path != 'MEMORY.md':
|
||||
path = f'memory/{path}'
|
||||
|
||||
file_path = workspace_dir / path
|
||||
|
||||
@@ -48,6 +48,13 @@ class MemorySearchTool(BaseTool):
|
||||
super().__init__()
|
||||
self.memory_manager = memory_manager
|
||||
self.user_id = user_id
|
||||
|
||||
from config import conf
|
||||
if conf().get("knowledge", True):
|
||||
self.description = (
|
||||
"Search agent's long-term memory and knowledge base using semantic and keyword search. "
|
||||
"Use this to recall past conversations, preferences, and knowledge pages."
|
||||
)
|
||||
|
||||
def execute(self, args: dict):
|
||||
"""
|
||||
|
||||
@@ -1038,6 +1038,7 @@ function startSSE(requestId, loadingEl, timestamp) {
|
||||
// Only update text content when there is something new to show.
|
||||
if (finalText) contentEl.innerHTML = renderMarkdown(finalText);
|
||||
applyHighlighting(botEl);
|
||||
bindChatKnowledgeLinks(botEl);
|
||||
}
|
||||
scrollChatToBottom();
|
||||
|
||||
@@ -1255,6 +1256,7 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
|
||||
</div>
|
||||
`;
|
||||
applyHighlighting(el);
|
||||
bindChatKnowledgeLinks(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -3002,6 +3004,92 @@ function filterKnowledgeTree(query) {
|
||||
renderKnowledgeTree(_knowledgeTreeData, query);
|
||||
}
|
||||
|
||||
function resolveKnowledgePath(currentFilePath, relativeHref) {
|
||||
// currentFilePath: e.g. "concepts/mcp-protocol.md"
|
||||
// relativeHref: e.g. "../entities/openai.md"
|
||||
const parts = currentFilePath.split('/');
|
||||
parts.pop(); // remove filename, keep directory
|
||||
const segments = [...parts, ...relativeHref.split('/')];
|
||||
const resolved = [];
|
||||
for (const seg of segments) {
|
||||
if (seg === '..') resolved.pop();
|
||||
else if (seg !== '.' && seg !== '') resolved.push(seg);
|
||||
}
|
||||
return resolved.join('/');
|
||||
}
|
||||
|
||||
function bindKnowledgeLinks(container, currentFilePath) {
|
||||
container.querySelectorAll('a').forEach(a => {
|
||||
const href = a.getAttribute('href');
|
||||
if (!href || !href.endsWith('.md')) return;
|
||||
// Skip absolute URLs
|
||||
if (/^https?:\/\//.test(href)) return;
|
||||
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const resolved = resolveKnowledgePath(currentFilePath, href);
|
||||
const linkTitle = a.textContent.trim() || resolved.replace(/\.md$/, '').split('/').pop();
|
||||
openKnowledgeFile(resolved, linkTitle);
|
||||
});
|
||||
a.style.cursor = 'pointer';
|
||||
a.classList.add('text-primary-500', 'hover:underline');
|
||||
});
|
||||
}
|
||||
|
||||
function bindChatKnowledgeLinks(container) {
|
||||
if (!container) return;
|
||||
container.querySelectorAll('a').forEach(a => {
|
||||
const href = a.getAttribute('href');
|
||||
if (!href || !href.endsWith('.md')) return;
|
||||
if (/^https?:\/\//.test(href)) return;
|
||||
|
||||
// Determine knowledge path
|
||||
let knowledgePath = null;
|
||||
if (href.startsWith('knowledge/')) {
|
||||
// Full path from workspace root: knowledge/concepts/moe.md
|
||||
knowledgePath = href.replace(/^knowledge\//, '');
|
||||
} else if (/^[a-z0-9_-]+\/[a-z0-9_.-]+\.md$/i.test(href)) {
|
||||
// Looks like category/file.md pattern without knowledge/ prefix
|
||||
knowledgePath = href;
|
||||
} else if (href.includes('/') && !href.startsWith('/')) {
|
||||
// Relative path like ../entities/deepseek.md — extract filename and search
|
||||
const filename = href.split('/').pop();
|
||||
knowledgePath = '__search__:' + filename;
|
||||
}
|
||||
if (!knowledgePath) return;
|
||||
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (knowledgePath.startsWith('__search__:')) {
|
||||
const filename = knowledgePath.replace('__search__:', '');
|
||||
// Find the file in cached tree data
|
||||
const found = _findKnowledgeFileByName(filename);
|
||||
if (found) {
|
||||
navigateTo('knowledge');
|
||||
setTimeout(() => openKnowledgeFile(found.path, found.title), 100);
|
||||
}
|
||||
} else {
|
||||
navigateTo('knowledge');
|
||||
const linkTitle = a.textContent.trim() || knowledgePath.replace(/\.md$/, '').split('/').pop();
|
||||
setTimeout(() => openKnowledgeFile(knowledgePath, linkTitle), 100);
|
||||
}
|
||||
});
|
||||
a.style.cursor = 'pointer';
|
||||
a.classList.add('text-primary-500', 'hover:underline');
|
||||
});
|
||||
}
|
||||
|
||||
function _findKnowledgeFileByName(filename) {
|
||||
for (const group of _knowledgeTreeData) {
|
||||
for (const f of group.files) {
|
||||
if (f.name === filename) {
|
||||
return { path: group.dir + '/' + f.name, title: f.title };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openKnowledgeFile(path, title) {
|
||||
_knowledgeCurrentFile = path;
|
||||
// Update active state in tree via data-path
|
||||
@@ -3017,9 +3105,11 @@ function openKnowledgeFile(path, title) {
|
||||
const viewer = document.getElementById('knowledge-content-viewer');
|
||||
document.getElementById('knowledge-viewer-title').textContent = title;
|
||||
document.getElementById('knowledge-viewer-path').textContent = path;
|
||||
document.getElementById('knowledge-viewer-body').innerHTML = renderMarkdown(data.content || '');
|
||||
const bodyEl = document.getElementById('knowledge-viewer-body');
|
||||
bodyEl.innerHTML = renderMarkdown(data.content || '');
|
||||
viewer.classList.remove('hidden');
|
||||
applyHighlighting(viewer);
|
||||
bindKnowledgeLinks(bodyEl, path);
|
||||
|
||||
// Mobile: hide sidebar, show content
|
||||
if (window.innerWidth < 768) {
|
||||
@@ -3234,6 +3324,12 @@ function renderKnowledgeGraph(container, nodes, links) {
|
||||
// =====================================================================
|
||||
applyTheme();
|
||||
applyI18n();
|
||||
|
||||
// Pre-fetch knowledge tree for chat link resolution
|
||||
fetch('/api/knowledge/list').then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') _knowledgeTreeData = data.tree || [];
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/api/version').then(r => r.json()).then(data => {
|
||||
APP_VERSION = `v${data.version}`;
|
||||
document.getElementById('sidebar-version').textContent = `CowAgent ${APP_VERSION}`;
|
||||
|
||||
@@ -88,3 +88,4 @@ Append-only, newest at bottom:
|
||||
- **Cross-reference**: every page should link to related pages; keep the knowledge graph connected
|
||||
- **Index is mandatory**: always update `knowledge/index.md` after any change
|
||||
- **Be concise**: capture essence, not copy entire sources
|
||||
- **Full paths in replies**: when referencing knowledge files in conversation replies, use the full path from workspace root (e.g. `[Title](knowledge/<category>/<slug>.md)`), not relative paths. Relative paths are only for cross-references inside knowledge pages themselves.
|
||||
|
||||
Reference in New Issue
Block a user