From 5a104760108dbf025a01ae43740c95f03479bfe8 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sat, 11 Apr 2026 16:44:25 +0800 Subject: [PATCH] feat: add knowledge switch and cli --- agent/memory/manager.py | 10 ++- agent/prompt/builder.py | 4 +- agent/prompt/workspace.py | 24 +++--- agent/skills/manager.py | 4 + channel/web/static/css/console.css | 3 +- channel/web/static/js/console.js | 4 + config.py | 1 + plugins/cow_cli/cow_cli.py | 132 +++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 16 deletions(-) diff --git a/agent/memory/manager.py b/agent/memory/manager.py index 967270f7..9141bc91 100644 --- a/agent/memory/manager.py +++ b/agent/memory/manager.py @@ -318,10 +318,12 @@ class MemoryManager: await self._sync_file(file_path, "memory", scope, user_id) # Scan knowledge directory (structured knowledge wiki) - knowledge_dir = Path(workspace_dir) / "knowledge" - if knowledge_dir.exists(): - for file_path in knowledge_dir.rglob("*.md"): - await self._sync_file(file_path, "knowledge", "shared", None) + from config import conf + if conf().get("knowledge", True): + knowledge_dir = Path(workspace_dir) / "knowledge" + if knowledge_dir.exists(): + for file_path in knowledge_dir.rglob("*.md"): + await self._sync_file(file_path, "knowledge", "shared", None) self._dirty = False diff --git a/agent/prompt/builder.py b/agent/prompt/builder.py index 11bd3b51..6535abc9 100644 --- a/agent/prompt/builder.py +++ b/agent/prompt/builder.py @@ -10,6 +10,7 @@ from typing import List, Dict, Optional, Any from dataclasses import dataclass from common.log import logger +from config import conf @dataclass @@ -129,7 +130,8 @@ def build_agent_system_prompt( sections.extend(_build_memory_section(memory_manager, tools, language)) # 3.5 知识系统(结构化知识库) - sections.extend(_build_knowledge_section(workspace_dir, language)) + if conf().get("knowledge", True): + sections.extend(_build_knowledge_section(workspace_dir, language)) # 4. 工作空间(工作环境说明) sections.extend(_build_workspace_section(workspace_dir, language)) diff --git a/agent/prompt/workspace.py b/agent/prompt/workspace.py index 60fdd762..797006ce 100644 --- a/agent/prompt/workspace.py +++ b/agent/prompt/workspace.py @@ -68,8 +68,11 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works websites_dir = os.path.join(workspace_dir, "websites") os.makedirs(websites_dir, exist_ok=True) - knowledge_dir = os.path.join(workspace_dir, "knowledge") - os.makedirs(knowledge_dir, exist_ok=True) + from config import conf + knowledge_enabled = conf().get("knowledge", True) + if knowledge_enabled: + knowledge_dir = os.path.join(workspace_dir, "knowledge") + os.makedirs(knowledge_dir, exist_ok=True) # 如果需要,创建模板文件 if create_templates: @@ -77,14 +80,15 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works _create_template_if_missing(user_path, _get_user_template()) _create_template_if_missing(rule_path, _get_rule_template()) _create_template_if_missing(memory_path, _get_memory_template()) - _create_template_if_missing( - os.path.join(knowledge_dir, "index.md"), - _get_knowledge_index_template() - ) - _create_template_if_missing( - os.path.join(knowledge_dir, "log.md"), - _get_knowledge_log_template() - ) + if knowledge_enabled: + _create_template_if_missing( + os.path.join(knowledge_dir, "index.md"), + _get_knowledge_index_template() + ) + _create_template_if_missing( + os.path.join(knowledge_dir, "log.md"), + _get_knowledge_log_template() + ) # Only create BOOTSTRAP.md for brand new workspaces; # agent deletes it after completing onboarding diff --git a/agent/skills/manager.py b/agent/skills/manager.py index c7daf7ad..ddb2a316 100644 --- a/agent/skills/manager.py +++ b/agent/skills/manager.py @@ -210,6 +210,10 @@ class SkillManager: if not include_disabled: entries = [e for e in entries if self.is_skill_enabled(e.skill.name)] + from config import conf + if not conf().get("knowledge", True): + entries = [e for e in entries if e.skill.name != "knowledge-wiki"] + return entries def filter_unavailable_skills( diff --git a/channel/web/static/css/console.css b/channel/web/static/css/console.css index 96b0811b..080309de 100644 --- a/channel/web/static/css/console.css +++ b/channel/web/static/css/console.css @@ -45,7 +45,8 @@ .msg-content h1 { font-size: 1.4em; } .msg-content h2 { font-size: 1.25em; } .msg-content h3 { font-size: 1.1em; } -.msg-content ul, .msg-content ol { margin: 0.5em 0; padding-left: 1.8em; } +.msg-content ul { margin: 0.5em 0; padding-left: 1.8em; list-style: disc; } +.msg-content ol { margin: 0.5em 0; padding-left: 1.8em; list-style: decimal; } .msg-content li { margin: 0.25em 0; } .msg-content pre { border-radius: 8px; overflow-x: auto; margin: 0.8em 0; diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js index 16c4c6c6..a571f6ee 100644 --- a/channel/web/static/js/console.js +++ b/channel/web/static/js/console.js @@ -496,6 +496,10 @@ const SLASH_COMMANDS = [ { cmd: '/skill info ', desc: '查看技能详情' }, { cmd: '/skill enable ', desc: '启用技能' }, { cmd: '/skill disable ', desc: '禁用技能' }, + { cmd: '/knowledge', desc: '查看知识库统计' }, + { cmd: '/knowledge list', desc: '查看知识库文件树' }, + { cmd: '/knowledge on', desc: '开启知识库' }, + { cmd: '/knowledge off', desc: '关闭知识库' }, { cmd: '/config', desc: '查看当前配置' }, { cmd: '/logs', desc: '查看最近日志' }, { cmd: '/version', desc: '查看版本' }, diff --git a/config.py b/config.py index 6edd9c04..ab7da486 100644 --- a/config.py +++ b/config.py @@ -199,6 +199,7 @@ available_setting = { "agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens "agent_max_context_turns": 30, # Agent模式下最大上下文记忆轮次 "agent_max_steps": 15, # Agent模式下单次运行最大决策步数 + "knowledge": True, # 是否开启知识库功能 } diff --git a/plugins/cow_cli/cow_cli.py b/plugins/cow_cli/cow_cli.py index 5f0da84f..d3a45349 100644 --- a/plugins/cow_cli/cow_cli.py +++ b/plugins/cow_cli/cow_cli.py @@ -31,6 +31,7 @@ KNOWN_COMMANDS = { "help", "version", "status", "logs", "start", "stop", "restart", "skill", "context", "config", + "knowledge", "install-browser", } @@ -157,6 +158,9 @@ class CowCliPlugin(Plugin): " /config 查看当前配置", " /config 查看某项配置", " /config 修改配置", + " /knowledge 查看知识库统计", + " /knowledge list 查看知识库文件树", + " /knowledge on|off 开启/关闭知识库", "", "💡 也可以用 cow 代替 /", ] @@ -310,6 +314,7 @@ class CowCliPlugin(Plugin): "agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps", + "knowledge", } _CONFIG_READABLE = _CONFIG_WRITABLE | {"channel_type"} @@ -851,6 +856,133 @@ class CowCliPlugin(Plugin): icon = "✅" if enabled else "⬚" return f"{icon} 技能 '{name}' 已{action}" + # ------------------------------------------------------------------ + # knowledge + # ------------------------------------------------------------------ + + def _cmd_knowledge(self, args: str, e_context, **_) -> str: + sub = args.strip().lower().split(None, 1)[0] if args.strip() else "" + + if sub == "on": + return self._knowledge_toggle(True) + elif sub == "off": + return self._knowledge_toggle(False) + elif sub in ("list", "tree"): + return self._knowledge_tree() + else: + return self._knowledge_stats() + + def _knowledge_toggle(self, enabled: bool) -> str: + from config import conf + import json as _json + + conf()["knowledge"] = enabled + + project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + config_path = os.path.join(project_root, "config.json") + try: + with open(config_path, "r", encoding="utf-8") as f: + file_config = _json.load(f) + file_config["knowledge"] = enabled + with open(config_path, "w", encoding="utf-8") as f: + _json.dump(file_config, f, indent=4, ensure_ascii=False) + except Exception as e: + return f"⚠️ 内存中已切换,但写入 config.json 失败: {e}" + + status = "开启 ✅" if enabled else "关闭 ❌" + note = "知识库将在下次对话中生效" if enabled else "知识库系统已停用,不再注入提示词和索引知识文件" + return f"📚 知识库已{status}\n\n{note}" + + def _knowledge_stats(self) -> str: + from config import conf + from common.utils import expand_path + knowledge_dir = os.path.join( + expand_path(conf().get("agent_workspace", "~/cow")), + "knowledge" + ) + if not os.path.isdir(knowledge_dir): + return "📚 知识库目录不存在\n\n💡 开启知识库: /knowledge on" + + enabled = conf().get("knowledge", True) + total_files = 0 + total_bytes = 0 + cat_count = {} + + for root, dirs, files in os.walk(knowledge_dir): + dirs[:] = [d for d in dirs if not d.startswith(".")] + rel_root = os.path.relpath(root, knowledge_dir) + category = rel_root.split(os.sep)[0] if rel_root != "." else "root" + for f in files: + if f.endswith(".md") and f not in ("index.md", "log.md"): + total_files += 1 + total_bytes += os.path.getsize(os.path.join(root, f)) + cat_count[category] = cat_count.get(category, 0) + 1 + + status = "✅ 已开启" if enabled else "❌ 已关闭" + lines = [ + "📚 知识库统计", + "", + f"状态: {status}", + f"页面: {total_files} 篇", + f"大小: {total_bytes / 1024:.1f} KB", + "", + ] + if cat_count: + for cat in sorted(cat_count.keys()): + lines.append(f"- {cat}/ ({cat_count[cat]} pages)") + lines.append("") + + lines.append(f"路径: {knowledge_dir}") + lines.extend([ + "", + "━━━━━━━━━━━━━━━━━━━━━━━━━━", + "💡 /knowledge list 查看文件树", + "💡 /knowledge on|off 开关知识库", + ]) + return "\n".join(lines) + + def _knowledge_tree(self) -> str: + from config import conf + from common.utils import expand_path + knowledge_dir = os.path.join( + expand_path(conf().get("agent_workspace", "~/cow")), + "knowledge" + ) + if not os.path.isdir(knowledge_dir): + return "📚 知识库目录不存在\n\n💡 开启知识库: /knowledge on" + + tree = ["knowledge/"] + + subdirs = sorted([ + d for d in os.listdir(knowledge_dir) + if os.path.isdir(os.path.join(knowledge_dir, d)) and not d.startswith(".") + ]) + + for i, subdir in enumerate(subdirs): + is_last_dir = (i == len(subdirs) - 1) + branch = "└── " if is_last_dir else "├── " + subdir_path = os.path.join(knowledge_dir, subdir) + md_files = sorted([ + f for f in os.listdir(subdir_path) + if f.endswith(".md") and not f.startswith(".") + ]) + tree.append(f"{branch}{subdir}/ ({len(md_files)})") + + child_prefix = " " if is_last_dir else "│ " + max_show = 12 + for j, fname in enumerate(md_files[:max_show]): + is_last_file = (j == len(md_files[:max_show]) - 1) and len(md_files) <= max_show + fb = "└── " if is_last_file else "├── " + name = fname.replace(".md", "") + tree.append(f"{child_prefix}{fb}{name}") + if len(md_files) > max_show: + tree.append(f"{child_prefix}└── ... +{len(md_files) - max_show} more") + + if not subdirs: + tree.append("(空)") + + return "```\n" + "\n".join(tree) + "\n```" + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------