feat: add knowledge switch and cli

This commit is contained in:
zhayujie
2026-04-11 16:44:25 +08:00
parent 845fadd0aa
commit 5a10476010
8 changed files with 166 additions and 16 deletions

View File

@@ -318,6 +318,8 @@ class MemoryManager:
await self._sync_file(file_path, "memory", scope, user_id) await self._sync_file(file_path, "memory", scope, user_id)
# Scan knowledge directory (structured knowledge wiki) # Scan knowledge directory (structured knowledge wiki)
from config import conf
if conf().get("knowledge", True):
knowledge_dir = Path(workspace_dir) / "knowledge" knowledge_dir = Path(workspace_dir) / "knowledge"
if knowledge_dir.exists(): if knowledge_dir.exists():
for file_path in knowledge_dir.rglob("*.md"): for file_path in knowledge_dir.rglob("*.md"):

View File

@@ -10,6 +10,7 @@ from typing import List, Dict, Optional, Any
from dataclasses import dataclass from dataclasses import dataclass
from common.log import logger from common.log import logger
from config import conf
@dataclass @dataclass
@@ -129,6 +130,7 @@ def build_agent_system_prompt(
sections.extend(_build_memory_section(memory_manager, tools, language)) sections.extend(_build_memory_section(memory_manager, tools, language))
# 3.5 知识系统(结构化知识库) # 3.5 知识系统(结构化知识库)
if conf().get("knowledge", True):
sections.extend(_build_knowledge_section(workspace_dir, language)) sections.extend(_build_knowledge_section(workspace_dir, language))
# 4. 工作空间(工作环境说明) # 4. 工作空间(工作环境说明)

View File

@@ -68,6 +68,9 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works
websites_dir = os.path.join(workspace_dir, "websites") websites_dir = os.path.join(workspace_dir, "websites")
os.makedirs(websites_dir, exist_ok=True) os.makedirs(websites_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") knowledge_dir = os.path.join(workspace_dir, "knowledge")
os.makedirs(knowledge_dir, exist_ok=True) os.makedirs(knowledge_dir, exist_ok=True)
@@ -77,6 +80,7 @@ def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> Works
_create_template_if_missing(user_path, _get_user_template()) _create_template_if_missing(user_path, _get_user_template())
_create_template_if_missing(rule_path, _get_rule_template()) _create_template_if_missing(rule_path, _get_rule_template())
_create_template_if_missing(memory_path, _get_memory_template()) _create_template_if_missing(memory_path, _get_memory_template())
if knowledge_enabled:
_create_template_if_missing( _create_template_if_missing(
os.path.join(knowledge_dir, "index.md"), os.path.join(knowledge_dir, "index.md"),
_get_knowledge_index_template() _get_knowledge_index_template()

View File

@@ -210,6 +210,10 @@ class SkillManager:
if not include_disabled: if not include_disabled:
entries = [e for e in entries if self.is_skill_enabled(e.skill.name)] 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 return entries
def filter_unavailable_skills( def filter_unavailable_skills(

View File

@@ -45,7 +45,8 @@
.msg-content h1 { font-size: 1.4em; } .msg-content h1 { font-size: 1.4em; }
.msg-content h2 { font-size: 1.25em; } .msg-content h2 { font-size: 1.25em; }
.msg-content h3 { font-size: 1.1em; } .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 li { margin: 0.25em 0; }
.msg-content pre { .msg-content pre {
border-radius: 8px; overflow-x: auto; margin: 0.8em 0; border-radius: 8px; overflow-x: auto; margin: 0.8em 0;

View File

@@ -496,6 +496,10 @@ const SLASH_COMMANDS = [
{ cmd: '/skill info ', desc: '查看技能详情' }, { cmd: '/skill info ', desc: '查看技能详情' },
{ cmd: '/skill enable ', desc: '启用技能' }, { cmd: '/skill enable ', desc: '启用技能' },
{ cmd: '/skill disable ', desc: '禁用技能' }, { cmd: '/skill disable ', desc: '禁用技能' },
{ cmd: '/knowledge', desc: '查看知识库统计' },
{ cmd: '/knowledge list', desc: '查看知识库文件树' },
{ cmd: '/knowledge on', desc: '开启知识库' },
{ cmd: '/knowledge off', desc: '关闭知识库' },
{ cmd: '/config', desc: '查看当前配置' }, { cmd: '/config', desc: '查看当前配置' },
{ cmd: '/logs', desc: '查看最近日志' }, { cmd: '/logs', desc: '查看最近日志' },
{ cmd: '/version', desc: '查看版本' }, { cmd: '/version', desc: '查看版本' },

View File

@@ -199,6 +199,7 @@ available_setting = {
"agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens "agent_max_context_tokens": 50000, # Agent模式下最大上下文tokens
"agent_max_context_turns": 30, # Agent模式下最大上下文记忆轮次 "agent_max_context_turns": 30, # Agent模式下最大上下文记忆轮次
"agent_max_steps": 15, # Agent模式下单次运行最大决策步数 "agent_max_steps": 15, # Agent模式下单次运行最大决策步数
"knowledge": True, # 是否开启知识库功能
} }

View File

@@ -31,6 +31,7 @@ KNOWN_COMMANDS = {
"help", "version", "status", "logs", "help", "version", "status", "logs",
"start", "stop", "restart", "start", "stop", "restart",
"skill", "context", "config", "skill", "context", "config",
"knowledge",
"install-browser", "install-browser",
} }
@@ -157,6 +158,9 @@ class CowCliPlugin(Plugin):
" /config 查看当前配置", " /config 查看当前配置",
" /config <key> 查看某项配置", " /config <key> 查看某项配置",
" /config <key> <val> 修改配置", " /config <key> <val> 修改配置",
" /knowledge 查看知识库统计",
" /knowledge list 查看知识库文件树",
" /knowledge on|off 开启/关闭知识库",
"", "",
"💡 也可以用 cow <command> 代替 /<command>", "💡 也可以用 cow <command> 代替 /<command>",
] ]
@@ -310,6 +314,7 @@ class CowCliPlugin(Plugin):
"agent_max_context_tokens", "agent_max_context_tokens",
"agent_max_context_turns", "agent_max_context_turns",
"agent_max_steps", "agent_max_steps",
"knowledge",
} }
_CONFIG_READABLE = _CONFIG_WRITABLE | {"channel_type"} _CONFIG_READABLE = _CONFIG_WRITABLE | {"channel_type"}
@@ -851,6 +856,133 @@ class CowCliPlugin(Plugin):
icon = "" if enabled else "" icon = "" if enabled else ""
return f"{icon} 技能 '{name}'{action}" 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 # Helpers
# ------------------------------------------------------------------ # ------------------------------------------------------------------