mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 06:07:13 +08:00
feat(knowledge): document link supports jumping to view
This commit is contained in:
@@ -45,6 +45,19 @@ class MemoryGetTool(BaseTool):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.memory_manager = memory_manager
|
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):
|
def execute(self, args: dict):
|
||||||
"""
|
"""
|
||||||
Execute memory file read
|
Execute memory file read
|
||||||
@@ -68,8 +81,8 @@ class MemoryGetTool(BaseTool):
|
|||||||
workspace_dir = self.memory_manager.config.get_workspace()
|
workspace_dir = self.memory_manager.config.get_workspace()
|
||||||
|
|
||||||
# Auto-prepend memory/ if not present and not absolute path
|
# Auto-prepend memory/ if not present and not absolute path
|
||||||
# Exception: MEMORY.md is in the root directory
|
# Exceptions: MEMORY.md in root, knowledge/ files at workspace root
|
||||||
if not path.startswith('memory/') and not path.startswith('/') and path != 'MEMORY.md':
|
if not path.startswith('memory/') and not path.startswith('knowledge/') and not path.startswith('/') and path != 'MEMORY.md':
|
||||||
path = f'memory/{path}'
|
path = f'memory/{path}'
|
||||||
|
|
||||||
file_path = workspace_dir / path
|
file_path = workspace_dir / path
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ class MemorySearchTool(BaseTool):
|
|||||||
self.memory_manager = memory_manager
|
self.memory_manager = memory_manager
|
||||||
self.user_id = user_id
|
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):
|
def execute(self, args: dict):
|
||||||
"""
|
"""
|
||||||
Execute memory search
|
Execute memory search
|
||||||
|
|||||||
@@ -1038,6 +1038,7 @@ function startSSE(requestId, loadingEl, timestamp) {
|
|||||||
// Only update text content when there is something new to show.
|
// Only update text content when there is something new to show.
|
||||||
if (finalText) contentEl.innerHTML = renderMarkdown(finalText);
|
if (finalText) contentEl.innerHTML = renderMarkdown(finalText);
|
||||||
applyHighlighting(botEl);
|
applyHighlighting(botEl);
|
||||||
|
bindChatKnowledgeLinks(botEl);
|
||||||
}
|
}
|
||||||
scrollChatToBottom();
|
scrollChatToBottom();
|
||||||
|
|
||||||
@@ -1255,6 +1256,7 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
applyHighlighting(el);
|
applyHighlighting(el);
|
||||||
|
bindChatKnowledgeLinks(el);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3002,6 +3004,92 @@ function filterKnowledgeTree(query) {
|
|||||||
renderKnowledgeTree(_knowledgeTreeData, 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) {
|
function openKnowledgeFile(path, title) {
|
||||||
_knowledgeCurrentFile = path;
|
_knowledgeCurrentFile = path;
|
||||||
// Update active state in tree via data-path
|
// Update active state in tree via data-path
|
||||||
@@ -3017,9 +3105,11 @@ function openKnowledgeFile(path, title) {
|
|||||||
const viewer = document.getElementById('knowledge-content-viewer');
|
const viewer = document.getElementById('knowledge-content-viewer');
|
||||||
document.getElementById('knowledge-viewer-title').textContent = title;
|
document.getElementById('knowledge-viewer-title').textContent = title;
|
||||||
document.getElementById('knowledge-viewer-path').textContent = path;
|
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');
|
viewer.classList.remove('hidden');
|
||||||
applyHighlighting(viewer);
|
applyHighlighting(viewer);
|
||||||
|
bindKnowledgeLinks(bodyEl, path);
|
||||||
|
|
||||||
// Mobile: hide sidebar, show content
|
// Mobile: hide sidebar, show content
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
@@ -3234,6 +3324,12 @@ function renderKnowledgeGraph(container, nodes, links) {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
applyTheme();
|
applyTheme();
|
||||||
applyI18n();
|
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 => {
|
fetch('/api/version').then(r => r.json()).then(data => {
|
||||||
APP_VERSION = `v${data.version}`;
|
APP_VERSION = `v${data.version}`;
|
||||||
document.getElementById('sidebar-version').textContent = `CowAgent ${APP_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
|
- **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
|
- **Index is mandatory**: always update `knowledge/index.md` after any change
|
||||||
- **Be concise**: capture essence, not copy entire sources
|
- **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