feat(knowledge): add cli and update docs

This commit is contained in:
zhayujie
2026-04-12 16:39:06 +08:00
parent ea78e3d0c6
commit a1d82f6193
34 changed files with 956 additions and 208 deletions

View File

@@ -1535,141 +1535,45 @@ class AssetsHandler:
class KnowledgeListHandler:
"""Return the knowledge directory tree as JSON."""
def GET(self):
web.header('Content-Type', 'application/json; charset=utf-8')
try:
workspace_root = _get_workspace_root()
knowledge_dir = os.path.join(workspace_root, "knowledge")
if not os.path.isdir(knowledge_dir):
return json.dumps({"status": "success", "tree": [], "stats": {"pages": 0, "size": 0}})
tree = []
total_files = 0
total_bytes = 0
for name in sorted(os.listdir(knowledge_dir)):
full = os.path.join(knowledge_dir, name)
if not os.path.isdir(full) or name.startswith("."):
continue
files = []
for fname in sorted(os.listdir(full)):
if fname.endswith(".md") and not fname.startswith("."):
fpath = os.path.join(full, fname)
size = os.path.getsize(fpath)
total_files += 1
total_bytes += size
title = fname.replace(".md", "")
try:
with open(fpath, "r", encoding="utf-8") as f:
first_line = f.readline().strip()
if first_line.startswith("# "):
title = first_line[2:].strip()
except Exception:
pass
files.append({"name": fname, "title": title, "size": size})
tree.append({"dir": name, "files": files})
return json.dumps({
"status": "success",
"tree": tree,
"stats": {"pages": total_files, "size": total_bytes},
"enabled": conf().get("knowledge", True),
}, ensure_ascii=False)
from agent.knowledge.service import KnowledgeService
svc = KnowledgeService(_get_workspace_root())
result = svc.list_tree()
return json.dumps({"status": "success", **result}, ensure_ascii=False)
except Exception as e:
logger.error(f"[WebChannel] Knowledge list error: {e}")
return json.dumps({"status": "error", "message": str(e)})
class KnowledgeReadHandler:
"""Read a single knowledge markdown file."""
def GET(self):
web.header('Content-Type', 'application/json; charset=utf-8')
try:
from agent.knowledge.service import KnowledgeService
params = web.input(path='')
rel_path = params.path
if not rel_path or ".." in rel_path:
return json.dumps({"status": "error", "message": "invalid path"})
workspace_root = _get_workspace_root()
full_path = os.path.join(workspace_root, "knowledge", rel_path)
full_path = os.path.normpath(full_path)
knowledge_dir = os.path.normpath(os.path.join(workspace_root, "knowledge"))
if not full_path.startswith(knowledge_dir):
return json.dumps({"status": "error", "message": "path outside knowledge dir"})
if not os.path.isfile(full_path):
return json.dumps({"status": "error", "message": "file not found"})
with open(full_path, "r", encoding="utf-8") as f:
content = f.read()
return json.dumps({"status": "success", "content": content, "path": rel_path}, ensure_ascii=False)
svc = KnowledgeService(_get_workspace_root())
result = svc.read_file(params.path)
return json.dumps({"status": "success", **result}, ensure_ascii=False)
except (ValueError, FileNotFoundError) as e:
return json.dumps({"status": "error", "message": str(e)})
except Exception as e:
logger.error(f"[WebChannel] Knowledge read error: {e}")
return json.dumps({"status": "error", "message": str(e)})
class KnowledgeGraphHandler:
"""Return nodes and links for the knowledge graph visualization."""
def GET(self):
web.header('Content-Type', 'application/json; charset=utf-8')
import re
from pathlib import Path
workspace_root = _get_workspace_root()
knowledge_dir = Path(workspace_root) / "knowledge"
if not knowledge_dir.is_dir():
try:
from agent.knowledge.service import KnowledgeService
svc = KnowledgeService(_get_workspace_root())
return json.dumps(svc.build_graph(), ensure_ascii=False)
except Exception as e:
logger.error(f"[WebChannel] Knowledge graph error: {e}")
return json.dumps({"nodes": [], "links": []})
nodes = {}
links = []
link_re = re.compile(r'\[([^\]]*)\]\(([^)]+\.md)\)')
for md_file in knowledge_dir.rglob("*.md"):
rel = str(md_file.relative_to(knowledge_dir))
if rel in ("index.md", "log.md"):
continue
parts = rel.split("/")
category = parts[0] if len(parts) > 1 else "root"
title = md_file.stem.replace("-", " ").title()
try:
content = md_file.read_text(encoding="utf-8")
first_line = content.strip().split("\n")[0]
if first_line.startswith("# "):
title = first_line[2:].strip()
except Exception:
pass
nodes[rel] = {"id": rel, "label": title, "category": category}
try:
content = md_file.read_text(encoding="utf-8")
for _, link_target in link_re.findall(content):
resolved = (md_file.parent / link_target).resolve()
try:
target_rel = str(resolved.relative_to(knowledge_dir))
except ValueError:
continue
if target_rel != rel:
links.append({"source": rel, "target": target_rel})
except Exception:
pass
valid_ids = set(nodes.keys())
links = [l for l in links if l["source"] in valid_ids and l["target"] in valid_ids]
seen = set()
deduped = []
for l in links:
key = tuple(sorted([l["source"], l["target"]]))
if key not in seen:
seen.add(key)
deduped.append(l)
return json.dumps({
"nodes": list(nodes.values()),
"links": deduped,
}, ensure_ascii=False)
class VersionHandler:
def GET(self):