mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat(knowledge): support nested directories in knowledge base listing and display
This commit is contained in:
@@ -34,7 +34,8 @@ class KnowledgeService:
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def list_tree(self) -> dict:
|
def list_tree(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Return the knowledge directory tree grouped by category.
|
Return the knowledge directory tree grouped by category,
|
||||||
|
supporting arbitrarily nested sub-directories.
|
||||||
|
|
||||||
Returns::
|
Returns::
|
||||||
|
|
||||||
@@ -44,10 +45,20 @@ class KnowledgeService:
|
|||||||
"dir": "concepts",
|
"dir": "concepts",
|
||||||
"files": [
|
"files": [
|
||||||
{"name": "moe.md", "title": "MoE", "size": 1234},
|
{"name": "moe.md", "title": "MoE", "size": 1234},
|
||||||
...
|
],
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dir": "platform",
|
||||||
|
"files": [],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"dir": "analysis",
|
||||||
|
"files": [{"name": "perf.md", ...}],
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...
|
|
||||||
],
|
],
|
||||||
"stats": {"pages": 15, "size": 32768},
|
"stats": {"pages": 15, "size": 32768},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -56,20 +67,30 @@ class KnowledgeService:
|
|||||||
if not os.path.isdir(self.knowledge_dir):
|
if not os.path.isdir(self.knowledge_dir):
|
||||||
return {"tree": [], "stats": {"pages": 0, "size": 0}, "enabled": conf().get("knowledge", True)}
|
return {"tree": [], "stats": {"pages": 0, "size": 0}, "enabled": conf().get("knowledge", True)}
|
||||||
|
|
||||||
tree = []
|
stats = {"pages": 0, "size": 0}
|
||||||
total_files = 0
|
tree = self._scan_dir(self.knowledge_dir, stats)
|
||||||
total_bytes = 0
|
|
||||||
for name in sorted(os.listdir(self.knowledge_dir)):
|
return {
|
||||||
full = os.path.join(self.knowledge_dir, name)
|
"tree": tree,
|
||||||
if not os.path.isdir(full) or name.startswith("."):
|
"stats": stats,
|
||||||
|
"enabled": conf().get("knowledge", True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _scan_dir(self, dir_path: str, stats: dict) -> list:
|
||||||
|
"""Recursively scan a directory and return list of group nodes."""
|
||||||
|
result = []
|
||||||
|
for name in sorted(os.listdir(dir_path)):
|
||||||
|
if name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
full = os.path.join(dir_path, name)
|
||||||
|
if os.path.isdir(full):
|
||||||
files = []
|
files = []
|
||||||
for fname in sorted(os.listdir(full)):
|
for fname in sorted(os.listdir(full)):
|
||||||
if fname.endswith(".md") and not fname.startswith("."):
|
|
||||||
fpath = os.path.join(full, fname)
|
fpath = os.path.join(full, fname)
|
||||||
|
if os.path.isfile(fpath) and fname.endswith(".md") and not fname.startswith("."):
|
||||||
size = os.path.getsize(fpath)
|
size = os.path.getsize(fpath)
|
||||||
total_files += 1
|
stats["pages"] += 1
|
||||||
total_bytes += size
|
stats["size"] += size
|
||||||
title = fname.replace(".md", "")
|
title = fname.replace(".md", "")
|
||||||
try:
|
try:
|
||||||
with open(fpath, "r", encoding="utf-8") as f:
|
with open(fpath, "r", encoding="utf-8") as f:
|
||||||
@@ -79,13 +100,9 @@ class KnowledgeService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
files.append({"name": fname, "title": title, "size": size})
|
files.append({"name": fname, "title": title, "size": size})
|
||||||
tree.append({"dir": name, "files": files})
|
children = self._scan_dir(full, stats)
|
||||||
|
result.append({"dir": name, "files": files, "children": children})
|
||||||
return {
|
return result
|
||||||
"tree": tree,
|
|
||||||
"stats": {"pages": total_files, "size": total_bytes},
|
|
||||||
"enabled": conf().get("knowledge", True),
|
|
||||||
}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# read — single file content
|
# read — single file content
|
||||||
|
|||||||
@@ -952,13 +952,13 @@
|
|||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
transition: transform 0.15s;
|
transition: transform 0.15s;
|
||||||
}
|
}
|
||||||
.knowledge-tree-group.open .chevron {
|
.knowledge-tree-group.open > .knowledge-tree-group-btn .chevron {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
.knowledge-tree-group-items {
|
.knowledge-tree-group-items {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.knowledge-tree-group.open .knowledge-tree-group-items {
|
.knowledge-tree-group.open > .knowledge-tree-group-items {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3620,19 +3620,28 @@ function renderKnowledgeTree(tree, filter) {
|
|||||||
const container = document.getElementById('knowledge-tree');
|
const container = document.getElementById('knowledge-tree');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
const lowerFilter = (filter || '').toLowerCase();
|
const lowerFilter = (filter || '').toLowerCase();
|
||||||
|
_renderKnowledgeGroups(container, tree, '', lowerFilter, 0);
|
||||||
|
}
|
||||||
|
|
||||||
tree.forEach(group => {
|
function _renderKnowledgeGroups(container, groups, parentPath, lowerFilter, depth) {
|
||||||
const files = group.files.filter(f =>
|
const indent = depth * 12;
|
||||||
|
groups.forEach(group => {
|
||||||
|
const groupPath = parentPath ? parentPath + '/' + group.dir : group.dir;
|
||||||
|
const files = (group.files || []).filter(f =>
|
||||||
!lowerFilter || f.title.toLowerCase().includes(lowerFilter) || f.name.toLowerCase().includes(lowerFilter)
|
!lowerFilter || f.title.toLowerCase().includes(lowerFilter) || f.name.toLowerCase().includes(lowerFilter)
|
||||||
);
|
);
|
||||||
if (files.length === 0 && lowerFilter) return;
|
const children = group.children || [];
|
||||||
|
const hasMatchingChildren = lowerFilter ? _hasFilterMatch(children, lowerFilter) : children.length > 0;
|
||||||
|
if (files.length === 0 && !hasMatchingChildren && lowerFilter) return;
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'knowledge-tree-group open';
|
div.className = 'knowledge-tree-group open';
|
||||||
|
|
||||||
|
const fileCount = _countFiles(group);
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'knowledge-tree-group-btn';
|
btn.className = 'knowledge-tree-group-btn';
|
||||||
btn.innerHTML = `<i class="fas fa-chevron-right chevron"></i><i class="fas fa-folder text-amber-400 text-[11px]"></i><span>${escapeHtml(group.dir)}</span><span class="ml-auto text-[10px] text-slate-400">${files.length}</span>`;
|
btn.style.paddingLeft = (8 + indent) + 'px';
|
||||||
|
btn.innerHTML = `<i class="fas fa-chevron-right chevron"></i><i class="fas fa-folder text-amber-400 text-[11px]"></i><span>${escapeHtml(group.dir)}</span><span class="ml-auto text-[10px] text-slate-400">${fileCount}</span>`;
|
||||||
btn.onclick = () => div.classList.toggle('open');
|
btn.onclick = () => div.classList.toggle('open');
|
||||||
div.appendChild(btn);
|
div.appendChild(btn);
|
||||||
|
|
||||||
@@ -3640,18 +3649,40 @@ function renderKnowledgeTree(tree, filter) {
|
|||||||
items.className = 'knowledge-tree-group-items';
|
items.className = 'knowledge-tree-group-items';
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
const fbtn = document.createElement('button');
|
const fbtn = document.createElement('button');
|
||||||
const fpath = group.dir + '/' + f.name;
|
const fpath = groupPath + '/' + f.name;
|
||||||
fbtn.className = 'knowledge-tree-file' + (_knowledgeCurrentFile === fpath ? ' active' : '');
|
fbtn.className = 'knowledge-tree-file' + (_knowledgeCurrentFile === fpath ? ' active' : '');
|
||||||
fbtn.dataset.path = fpath;
|
fbtn.dataset.path = fpath;
|
||||||
|
fbtn.style.paddingLeft = (24 + indent) + 'px';
|
||||||
fbtn.innerHTML = `<i class="fas fa-file-lines text-[10px] text-slate-400"></i><span class="truncate">${escapeHtml(f.title)}</span>`;
|
fbtn.innerHTML = `<i class="fas fa-file-lines text-[10px] text-slate-400"></i><span class="truncate">${escapeHtml(f.title)}</span>`;
|
||||||
fbtn.onclick = () => openKnowledgeFile(fpath, f.title);
|
fbtn.onclick = () => openKnowledgeFile(fpath, f.title);
|
||||||
items.appendChild(fbtn);
|
items.appendChild(fbtn);
|
||||||
});
|
});
|
||||||
|
if (children.length > 0) {
|
||||||
|
_renderKnowledgeGroups(items, children, groupPath, lowerFilter, depth + 1);
|
||||||
|
}
|
||||||
div.appendChild(items);
|
div.appendChild(items);
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _hasFilterMatch(groups, lowerFilter) {
|
||||||
|
for (const g of groups) {
|
||||||
|
for (const f of (g.files || [])) {
|
||||||
|
if (f.title.toLowerCase().includes(lowerFilter) || f.name.toLowerCase().includes(lowerFilter)) return true;
|
||||||
|
}
|
||||||
|
if (_hasFilterMatch(g.children || [], lowerFilter)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _countFiles(group) {
|
||||||
|
let count = (group.files || []).length;
|
||||||
|
for (const child of (group.children || [])) {
|
||||||
|
count += _countFiles(child);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
function filterKnowledgeTree(query) {
|
function filterKnowledgeTree(query) {
|
||||||
renderKnowledgeTree(_knowledgeTreeData, query);
|
renderKnowledgeTree(_knowledgeTreeData, query);
|
||||||
}
|
}
|
||||||
@@ -3732,12 +3763,19 @@ function bindChatKnowledgeLinks(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _findKnowledgeFileByName(filename) {
|
function _findKnowledgeFileByName(filename) {
|
||||||
for (const group of _knowledgeTreeData) {
|
return _searchFileInGroups(_knowledgeTreeData, '', filename);
|
||||||
for (const f of group.files) {
|
}
|
||||||
|
|
||||||
|
function _searchFileInGroups(groups, parentPath, filename) {
|
||||||
|
for (const group of groups) {
|
||||||
|
const groupPath = parentPath ? parentPath + '/' + group.dir : group.dir;
|
||||||
|
for (const f of (group.files || [])) {
|
||||||
if (f.name === filename) {
|
if (f.name === filename) {
|
||||||
return { path: group.dir + '/' + f.name, title: f.title };
|
return { path: groupPath + '/' + f.name, title: f.title };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const found = _searchFileInGroups(group.children || [], groupPath, filename);
|
||||||
|
if (found) return found;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ Maintain a persistent, structured knowledge base in the `knowledge/` directory.
|
|||||||
```markdown
|
```markdown
|
||||||
# Page Title
|
# Page Title
|
||||||
|
|
||||||
|
> Source: <URL or description of the original material>
|
||||||
|
|
||||||
Content here. Cross-reference related pages with markdown links:
|
Content here. Cross-reference related pages with markdown links:
|
||||||
[Related Page](../category/related-page.md)
|
[Related Page](../category/related-page.md)
|
||||||
|
|
||||||
@@ -53,6 +55,8 @@ Content here. Cross-reference related pages with markdown links:
|
|||||||
- [Page B](../category/page-b.md) — how it relates
|
- [Page B](../category/page-b.md) — how it relates
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `> Source:` line records where the knowledge came from (URL, document name, conversation, etc.). Always include it when the material originates from a specific source.
|
||||||
|
|
||||||
Cross-references build a knowledge graph. When creating or updating a page, link to related pages and update those pages to link back. **Only link to pages that already exist** — if a concept deserves its own page, create it first, then add the link.
|
Cross-references build a knowledge graph. When creating or updating a page, link to related pages and update those pages to link back. **Only link to pages that already exist** — if a concept deserves its own page, create it first, then add the link.
|
||||||
|
|
||||||
## Index Format (`knowledge/index.md`)
|
## Index Format (`knowledge/index.md`)
|
||||||
|
|||||||
Reference in New Issue
Block a user