diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore deleted file mode 100644 index 9de0f16..0000000 --- a/.codegraph/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# CodeGraph data files -# These are local to each machine and should not be committed - -# Database -*.db -*.db-wal -*.db-shm - -# Cache -cache/ - -# Logs -*.log - -# Hook markers -.dirty diff --git a/.codegraph/daemon.pid b/.codegraph/daemon.pid deleted file mode 100644 index 7545668..0000000 --- a/.codegraph/daemon.pid +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pid": 90337, - "version": "0.9.9", - "socketPath": "/Users/chrismoray/Desktop/Moray/MyOpenSource/MyKnowledge_PlatForm/.codegraph/daemon.sock", - "startedAt": 1785980718159 -} diff --git a/.githooks/post-merge b/.githooks/post-merge new file mode 100755 index 0000000..52ee459 --- /dev/null +++ b/.githooks/post-merge @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# MyKnowledge CodeGraph 索引同步 +# +# git pull / git merge 完成后自动运行 codegraph sync, +# 把远端合入的新代码增量同步进本地 CodeGraph 索引, +# 避免探查时用到过期的符号/引用。 +# +# 启用(一次性,本地配置不进 git): +# git config core.hooksPath .githooks +# +# 说明: +# - codegraph 不存在时静默跳过(不影响正常合并)。 +# - sync 失败不阻断流程(仅告警),因为索引过期不会损坏仓库。 +# - 跳过:git merge --no-verify 不会生效(post-merge 不支持 --no-verify), +# 如需临时跳过,可手动注释或在调用前 unset 环境变量。 + +set -u + +ROOT="$(git rev-parse --show-toplevel)" + +if ! command -v codegraph >/dev/null 2>&1; then + echo "→ codegraph 未安装,跳过索引同步" + exit 0 +fi + +echo "→ 同步 CodeGraph 索引(codegraph sync)..." +# codegraph sync 的 path 是位置参数(非 --path 选项);-q 为 git hook 场景设计。 +if codegraph sync -q "$ROOT"; then + echo "✓ CodeGraph 索引已更新" +else + echo "⚠ codegraph sync 失败,索引可能过期(不影响仓库),可稍后手动运行 codegraph sync" >&2 +fi + +exit 0 diff --git a/.githooks/pre-push b/.githooks/pre-push index a211bb4..ffe3163 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -64,4 +64,19 @@ if [ "$py_ver" != "$js_ver" ] \ fi echo "✓ 版本一致:v$py_ver" + +# 版本守门通过后,同步 CodeGraph 索引(增量更新,成本低)。 +# codegraph 不存在或 sync 失败时均不阻断 push(仅告警)。 +if command -v codegraph >/dev/null 2>&1; then + echo "→ 同步 CodeGraph 索引(codegraph sync)..." + # codegraph sync 的 path 是位置参数(非 --path 选项);-q 为 git hook 场景设计。 + if codegraph sync -q "$ROOT"; then + echo "✓ CodeGraph 索引已更新" + else + echo "⚠ codegraph sync 失败,索引可能过期(不影响 push),可稍后手动运行 codegraph sync" >&2 + fi +else + echo "→ codegraph 未安装,跳过索引同步" +fi + exit 0 diff --git a/.gitignore b/.gitignore index a84d356..5628794 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ .workbuddy/ .vscode/ .idea/ +.codegraph/ *.swp *.swo *~ @@ -20,6 +21,7 @@ Thumbs.db # Test / local KB .myknowledge_test/ +.pytest_cache/ # Build artifacts frontend/index.standalone.html @@ -29,10 +31,15 @@ frontend/index.standalone.html dist-backend/ /build/ *.spec +!myknowledge-backend.spec desktop/node_modules/ desktop/dist/ desktop/assets/icon.icns +# Enterprise configs (per-customer; not versioned — keep template.json + README.md) +desktop/enterprises/*.json +!desktop/enterprises/template.json + # Design artifacts (generated SVG/PNG, not versioned; keep SPEC.md/.md docs versioned) docs/designs/**/export/ docs/designs/**/screenshots/ diff --git a/README.md b/README.md index 1df5ab3..9aed3fc 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,46 @@ Run tests: pytest tests/ -v ``` +## Desktop App & Enterprise Packaging + +### Desktop App + +```bash +cd desktop +npm install # once +npm run release # default build (all platforms) → desktop/dist/ +``` + +### Enterprise Customization + +Enterprise builds merge per-customer platform whitelists (`enabled`/`display` +overrides) into the packaged `platforms.json`. + +1. **Create the enterprise config** `desktop/enterprises/.json`, modeled + on `desktop/enterprises/template.json`. + > Enterprise configs are git-ignored (`desktop/enterprises/*.json`, only + > `template.json` is tracked) — they never enter the repository. +2. **Build explicitly** (enterprise configs are never auto-applied): + + ```bash + cd desktop + npm run release -- --enterprise + # or directly: + bash scripts/release.sh --enterprise + ``` + +3. **Optional: local shell aliases** (not committed) for one-word builds — + one alias per enterprise: + + ```bash + # ~/.zshrc + alias release:apple='cd /path/to/MyKnowledge_PlatForm/desktop && npm run release -- --enterprise Apple' + alias release:xiaomi='cd /path/to/MyKnowledge_PlatForm/desktop && npm run release -- --enterprise Xiaomi' + ``` + + Then run `release:apple` / `release:xiaomi`. Multiple enterprise configs can + coexist in `desktop/enterprises/`; each is selected explicitly by name. + ## Self-Installation MyKnowledge comes with a complete AI installation guide (`docs/AI-SETUP.md`). Copy it to any MCP-compatible AI agent, and it will: @@ -137,7 +177,7 @@ MyKnowledge comes with a complete AI installation guide (`docs/AI-SETUP.md`). Co ## Versioning -- **System version**: defined in `backend/__version__.py` (currently 0.7.6) +- **System version**: defined in `backend/__version__.py` (currently 0.7.7) - **KB version**: git commit hash from `agent-commit.txt` checkpoint ## License diff --git a/README.zh.md b/README.zh.md index 5a1b2ea..1882c0a 100644 --- a/README.zh.md +++ b/README.zh.md @@ -135,9 +135,48 @@ pip install -e . pytest tests/ -v ``` +## 桌面 App 与企业定制打包 + +### 桌面 App + +```bash +cd desktop +npm install # 首次 +npm run release # 默认打包(全平台)→ desktop/dist/ +``` + +### 企业定制 + +企业打包会把每家客户启用的平台白名单(`enabled`/`display` 覆盖)合并进打包 +产物的 `platforms.json`。 + +1. **创建企业配置** `desktop/enterprises/<企业名>.json`,参照 + `desktop/enterprises/template.json` 编写。 + > 企业配置被 git 忽略(`desktop/enterprises/*.json`,只有 `template.json` + > 入库)——**企业配置永远不会进代码仓库**。 +2. **显式声明打包**(企业配置不会被自动使用): + + ```bash + cd desktop + npm run release -- --enterprise <企业名> + # 或直接: + bash scripts/release.sh --enterprise <企业名> + ``` + +3. **可选:本地 shell alias**(不入库)实现一键打包——每个企业一条: + + ```bash + # ~/.zshrc + alias release:apple='cd /path/to/MyKnowledge_PlatForm/desktop && npm run release -- --enterprise Apple' + alias release:xiaomi='cd /path/to/MyKnowledge_PlatForm/desktop && npm run release -- --enterprise Xiaomi' + ``` + + 之后直接 `release:apple` / `release:xiaomi`。多个企业配置可共存于 + `desktop/enterprises/`,按名字显式选择。 + ## 版本 -- **系统版本**:定义在 `backend/__version__.py`(当前 0.7.6) +- **系统版本**:定义在 `backend/__version__.py`(当前 0.7.7) - **知识库版本**:从 `agent-commit.txt` checkpoint 读取的 git commit hash ## 许可 diff --git a/backend/AiClientConfig/agents/MyKnowledge-agent-Enchante.md b/backend/AiClientConfig/agents/MyKnowledge-agent-Enchante.md new file mode 100644 index 0000000..c131d09 --- /dev/null +++ b/backend/AiClientConfig/agents/MyKnowledge-agent-Enchante.md @@ -0,0 +1,15 @@ +# MyKnowledge Agent + +你是 MyKnowledge 本地知识库(Markdown + Git)的专业 Agent,通过 MyKnowledge MCP 工具读写文档并维护知识库结构。 + +## 边界与规范 + +- 一切操作仅限本地知识库 root 内;只允许在 `common-knowledge/`、`projects/`、`archive/` 开头的路径下写入;禁止 `..` 路径穿越与绝对路径。 +- 写入会自动重建父级 readme 并提交 git;不要手动伪造 frontmatter,需保持文档为合法 Markdown。 +- 不删除或改名已有文档的 frontmatter 必需字段;新增字段安全,删改字段需评估影响。 +- 只操作本地知识库,不访问网络、不改写平台自身配置。 +- 遇到不确定的写入(覆盖、删除、改名),先说明后果并征得同意再执行。 + +## 工具 + +你的全部工具来自 **MyKnowledge** 这个 MCP server(操作本地知识库):用 `mcp_get_tool_description` 查看 MyKnowledge 提供的工具清单与用法,用 `mcp_call_tool` 调用;结构异常时用 `maint__knowledgebase_diagnose` 定位。 diff --git a/backend/AiClientConfig/agents/MyKnowledge-agent.md b/backend/AiClientConfig/agents/MyKnowledge-agent.md index 6e41e58..3bc47be 100644 --- a/backend/AiClientConfig/agents/MyKnowledge-agent.md +++ b/backend/AiClientConfig/agents/MyKnowledge-agent.md @@ -13,17 +13,20 @@ MyKnowledge 是一个纯 Markdown + Git 的本地知识库平台。你的职责 - **检索与导航**:`nav__list_dir` / `nav__get_document` / `nav__find` — 定位目录、读取文档、按条件查找。 - **文档写入**:`write__create_document` / `write__update_document` — 新建 / 更新文档(自动生成 id 与 frontmatter)。 -- **结构维护**:`maint__knowledgebase_diagnose` — 检测知识库结构问题。 +- **未提交检查**:`maint__check_uncommitted` — 检查工作区是否有未 commit 的改动(用户前端 REST 保存的临时草稿)。 +- **结构健康**:`maint__knowledgebase_diagnose` — 检测知识库结构问题(低频例行)。 - **工具能力**:`mcp_get_tool_description` / `mcp_call_tool` — 了解并使用各工具。 - **回滚与恢复**(如可用):`maint__list_trash` / `write__restore_document` — 处理误删恢复。 ## 工作流程 -1. **先建立上下文**:操作前先用 `nav__get_document` 读取知识库根 readme,了解整体结构,避免凭猜测定位。 -2. **定位再写入**:写操作前先用 `nav__list_dir` / `nav__find` 确认目标路径存在且正确,不盲目新建目录或覆盖文档。 -3. **写入即维护**:`write__create_document` / `write__update_document` 会自动重建父级 readme 并提交 git,无需手动处理。 -4. **诊断兜底**:结构异常(缺 readme、路径错乱)时用 `maint__knowledgebase_diagnose` 定位问题。 -5. **完成报告**:每次任务结束,向使用者汇报:改动了哪些文档 / 目录、是否触发重建与提交、遇到的边界问题。 +1. **先查未提交改动**:介入任务前先调用 `maint__check_uncommitted`,了解工作区是否有未 commit 的临时草稿。这是「待处理清单」——只做一句话播报,不阻塞、不主动追问,等用户明确要求「提交 / 整理」时才走写入流程。避免覆盖或遗漏用户已在前端保存但未正式化的内容。 +2. **低频健康检查**:每个会话开始时自问「距上次 `maint__knowledgebase_diagnose` 是否已超过 1 小时」,超时才跑一次;1 小时内不重复,避免每次对话都全库扫描。有结构问题(缺 readme、路径错乱)时向用户简要播报,不擅自修复。 +3. **先建立上下文**:操作前先用 `nav__get_document` 读取知识库根 readme,了解整体结构,避免凭猜测定位。 +4. **定位再写入**:写操作前先用 `nav__list_dir` / `nav__find` 确认目标路径存在且正确,不盲目新建目录或覆盖文档。 +5. **写入即维护**:`write__create_document` / `write__update_document` 会自动重建父级 readme 并提交 git,无需手动处理。 +6. **诊断兜底**:结构异常(缺 readme、路径错乱)时用 `maint__knowledgebase_diagnose` 定位问题。 +7. **完成报告**:每次任务结束,向使用者汇报:改动了哪些文档 / 目录、是否触发重建与提交、遇到的边界问题。 ## 路径与写入规范 diff --git a/backend/AiClientConfig/agents/frontmatter.json b/backend/AiClientConfig/agents/frontmatter.json index 446d07c..d58831e 100644 --- a/backend/AiClientConfig/agents/frontmatter.json +++ b/backend/AiClientConfig/agents/frontmatter.json @@ -7,7 +7,7 @@ "frontmatter": { "name": "MyKnowledge 知识管理专家", "description": "MyKnowledge 知识管理平台协作 Agent:通过 MCP 检索与维护本地知识库", - "tools": "mcp_get_tool_description, mcp_call_tool, nav__list_dir, nav__get_document, nav__find, write__create_document, write__update_document, maint__knowledgebase_diagnose", + "tools": "mcp_get_tool_description, mcp_call_tool, nav__list_dir, nav__get_document, nav__find, write__create_document, write__update_document, maint__knowledgebase_diagnose, maint__check_uncommitted", "model": "inherit" } }, @@ -16,7 +16,7 @@ "frontmatter": { "name": "MyKnowledge 知识管理专家", "description": "MyKnowledge 知识管理平台协作 Agent:通过 MCP 检索与维护本地知识库", - "tools": "mcp_get_tool_description, mcp_call_tool, nav__list_dir, nav__get_document, nav__find, write__create_document, write__update_document, maint__knowledgebase_diagnose", + "tools": "mcp_get_tool_description, mcp_call_tool, nav__list_dir, nav__get_document, nav__find, write__create_document, write__update_document, maint__knowledgebase_diagnose, maint__check_uncommitted", "model": "inherit", "agentMode": "manual", "enabled": true, diff --git a/backend/AiClientConfig/hooks/WorkBuddy.json b/backend/AiClientConfig/hooks/WorkBuddy.json index 9435da3..8a3bd36 100644 --- a/backend/AiClientConfig/hooks/WorkBuddy.json +++ b/backend/AiClientConfig/hooks/WorkBuddy.json @@ -4,10 +4,10 @@ "supports_hooks": true, "event": "PreToolUse", "matcher": "Bash|Write|Edit", - "matcher_note": "与 Claude Code 同协议:PreToolUse 按工具名触发,必须同时覆盖 Bash 与 Write/Edit(否则 file_write 拦截分支死代码)。MCP 调用由 hooks.py 内部放行。", - "command": "curl -s -X POST http://127.0.0.1:8080/hooks/pre-tool-use -H 'Content-Type: application/json' -d '$CLAUDE_TOOL_USE_INPUT'", - "protocol": "同 Claude Code:环境变量 $CLAUDE_TOOL_USE_INPUT 传 JSON,stdout 输出 JSON 决策。", + "matcher_note": "与 Claude Code 同协议:PreToolUse 按工具名触发,必须覆盖 Bash 与 Write/Edit(否则 file_write 拦截分支死代码)。MCP 调用由 hooks.py 内部放行。", + "command": "curl -s -X POST http://127.0.0.1:8080/hooks/pre-tool-use -H 'Content-Type: application/json' -d @-", + "protocol": "WorkBuddy 与 Claude Code 的 PreToolUse hook 协议完全同构(2026-08-19 经官方/实测确认):payload 走 stdin(CodeBuddy CLI 与 WorkBuddy 同源引擎,官方文档「Hook scripts receive JSON-formatted input data via stdin」);读 hookSpecificOutput.permissionDecision 做 allow/deny(另支持 ask 三态,我们只回 allow/deny 兼容);退出码 0=放行(按 stdout JSON)/1=非阻断放行/2=阻断/其它=放行。无 $WORKBUDDY_TOOL_USE_INPUT 环境变量传 payload。后端不依赖 tool_use_id/call_id(只读 tool_name/tool_input/cwd),字段名差异无影响。", "exit_code_deny": 2, "fail_open": true, - "notes": "写入 ~/.workbuddy/settings.json 的 hooks.PreToolUse。命令与 ClaudeCode 相同。" + "notes": "写入 ~/.workbuddy/settings.json 的 hooks.PreToolUse(非 ~/.claude/),改完须完全重启 WorkBuddy 才加载。命令与 ClaudeCode 相同(-d @- 从 stdin 读 payload)。阻断判定靠 stdout JSON 的 permissionDecision 而非 curl 退出码;后端不可达时 curl 退出非 0 → 放行(fail-open),符合审计日志场景。" } diff --git a/backend/AiClientConfig/platforms.json b/backend/AiClientConfig/platforms.json index e81cabf..cdf9ab2 100644 --- a/backend/AiClientConfig/platforms.json +++ b/backend/AiClientConfig/platforms.json @@ -1,11 +1,23 @@ { "_comment": "AI 客户端平台 → 各操作系统下的配置文件路径(单一来源)。平台 key 用 PascalCase,与 client_config.PLATFORMS 及前端 store.clientPlatforms 严格一致。路径占位符:~ → Path.home(),%APPDATA% / %USERPROFILE% → 对应环境变量。kinds 声明该平台支持的配置种类(mcp/hooks/agent);Enchante 的 MCP/Agent 均走 deeplink(无本地配置文件/agents 目录)。cli_names 用于 client_installed 检测(PATH 探测)。", - "os_order": ["macos", "windows", "linux"], + "os_order": [ + "macos", + "windows", + "linux" + ], "platforms": { "ClaudeCode": { "display": "Claude Code", - "kinds": ["mcp", "hooks", "agent"], - "cli_names": { "macos": "claude", "windows": "claude.exe", "linux": "claude" }, + "kinds": [ + "mcp", + "hooks", + "agent" + ], + "cli_names": { + "macos": "claude", + "windows": "claude.exe", + "linux": "claude" + }, "paths": { "macos": { "config_dir": "~/.claude", @@ -25,11 +37,14 @@ "settings_file": "~/.claude/settings.json", "agents_dir": "~/.claude/agents" } - } + }, + "enabled": true }, "ClaudeDesktop": { "display": "Claude Desktop", - "kinds": ["mcp"], + "kinds": [ + "mcp" + ], "cli_names": {}, "paths": { "macos": { @@ -44,11 +59,16 @@ "config_dir": "~/.config/Claude", "mcp_file": "~/.config/Claude/claude_desktop_config.json" } - } + }, + "enabled": true }, "CodeBuddyIDE": { "display": "CodeBuddy IDE", - "kinds": ["mcp", "hooks", "agent"], + "kinds": [ + "mcp", + "hooks", + "agent" + ], "cli_names": {}, "paths": { "macos": { @@ -57,11 +77,16 @@ "settings_file": "~/.codebuddy/settings.json", "agents_dir": "~/.codebuddy/agents" } - } + }, + "enabled": true }, "WorkBuddy": { "display": "WorkBuddy", - "kinds": ["mcp", "hooks", "agent"], + "kinds": [ + "mcp", + "hooks", + "agent" + ], "cli_names": {}, "paths": { "macos": { @@ -70,21 +95,30 @@ "settings_file": "~/.workbuddy/settings.json", "agents_dir": "~/.workbuddy/agents" } - } + }, + "enabled": true }, "Enchante": { "display": "Enchanté", - "kinds": ["mcp", "agent"], + "kinds": [ + "mcp", + "agent" + ], "cli_names": {}, "paths": { "macos": { "config_dir": "/Applications" } - } + }, + "enabled": true }, "Cursor": { "display": "Cursor", - "kinds": ["mcp", "hooks", "agent"], + "kinds": [ + "mcp", + "hooks", + "agent" + ], "cli_names": {}, "paths": { "macos": { @@ -93,7 +127,8 @@ "hooks_file": "~/.cursor/hooks.json", "agents_dir": "~/.cursor/agents" } - } + }, + "enabled": true } } } diff --git a/backend/__version__.py b/backend/__version__.py index 24c17e0..f8fa95c 100644 --- a/backend/__version__.py +++ b/backend/__version__.py @@ -1,2 +1,2 @@ """MyKnowledge version.""" -__version__ = "0.7.6" +__version__ = "0.7.7" diff --git a/backend/client_config.py b/backend/client_config.py index 56216eb..76a6b12 100644 --- a/backend/client_config.py +++ b/backend/client_config.py @@ -74,8 +74,45 @@ def _load_platforms_data() -> dict: # 平台枚举从 platforms.json 派生(单一来源)。 +# enabled=false 的平台不出现在 PLATFORMS(企业定制打包时被过滤, +# 前端列表/检测/生成均自动只覆盖启用平台)。 _platforms_data = _load_platforms_data() -PLATFORMS = tuple(_platforms_data["platforms"].keys()) +PLATFORMS = tuple( + key for key, spec in _platforms_data["platforms"].items() + if spec.get("enabled", True) +) + + +def platforms_meta() -> dict: + """Return display metadata for enabled platforms. + + ``{key: {"display": str, "enabled": bool, "order": int|None, "kinds": [...], ...}, ...}`` + — 供前端把展示名/能力集改为从后端配置读取(企业定制 display / kinds 后前端 + 无需改代码),并用 ``order`` 重排展示顺序(数字小的靠前;None/缺失排在后面 + 保持原序)。只含 enabled 平台。 + + 动态读 ``_load_platforms_data()``(而非模块级 ``_platforms_data`` 快照), + 保证与 PLATFORMS 过滤行为一致。 + """ + items = [] + for key, spec in _load_platforms_data()["platforms"].items(): + if not spec.get("enabled", True): + continue + meta = { + "display": spec.get("display", key), + "enabled": True, + "order": spec.get("order"), + "kinds": spec.get("kinds", []), + } + # 透传企业可定制的附加展示字段(如 kinds 之外的说明文案) + for f in ("native_hint",): + if f in spec: + meta[f] = spec[f] + items.append((key, meta)) + # 按 order 升序:有 order 的靠前(数字小优先),无 order(None)排在后面 + # 保持相对原序(Python sort 稳定)。 + items.sort(key=lambda kv: (kv[1]["order"] if kv[1]["order"] is not None else 10**9)) + return dict(items) def _kinds_for(platform: str) -> tuple: @@ -205,17 +242,22 @@ def enchante_deeplink() -> str: def _base64_quote(payload: dict) -> str: """URL-quote a JSON dict as an Enchante deeplink ``config`` query value. - Shared by the MCP and agent deeplinks: base64-encode the JSON, then force - ``+``→``%2B`` (Enchanté's Swift ``URLComponents`` would misread a raw ``+`` - as a space). Generation only — the actual deeplink capture is handled by - the Enchante client, not the backend. + Shared by the MCP and agent deeplinks: base64-encode the JSON, then URL-quote + it so every character that is not unreserved (``A-Z a-z 0-9 - . _ ~``) is + percent-encoded — including ``+``→``%2B``, ``/``→``%2F`` and ``=``→``%3D``. + Enchanté's Swift ``URLComponents`` parses the ``config`` query value and + would otherwise misread a raw ``+`` as a space or treat a raw ``/``/``=`` + ambiguously; percent-encoding the whole base64 alphabet guarantees a + lossless round-trip. Generation only — the actual deeplink capture is + handled by the Enchante client, not the backend. """ import base64 import urllib.parse enc = base64.b64encode(json.dumps(payload, ensure_ascii=False) .encode("utf-8")).decode("ascii") - return urllib.parse.quote( - enc, safe="-._~!$&'()*,;=:@/?") + # safe="-._~" keeps only unreserved chars literal; base64's + / = all get + # percent-encoded (safe set intentionally excludes them). + return urllib.parse.quote(enc, safe="-._~") def enchante_agent_deeplink() -> str: @@ -226,9 +268,11 @@ def enchante_agent_deeplink() -> str: Creates a one-click dedicated "MyKnowledge 知识管理专家" role in Enchanté's top Agent dropdown. Payload schema confirmed with Enchante (2026-08-19): - ``role``: the agent persona / system instructions, reusing - ``_agent_template()`` (``MyKnowledge-agent.md``) **as plain text — no - YAML frontmatter** (Enchanté injects it verbatim as the system prompt; - tool binding is carried structurally by ``mcpServers``). + ``_agent_template("Enchante")`` (``MyKnowledge-agent-Enchante.md``, 精简版) + **as plain text — no YAML frontmatter** (Enchanté injects it verbatim as + the system prompt; tool binding is carried structurally by ``mcpServers``). + The 精简版 keeps the deeplink short (the full template made the encoded + link ~4206 chars; the 精简版 drops it to ~2112 / ~2180 re-encoded). - ``skillNames``: optional skill whitelist. **Empty ``[]``** — MyKnowledge no longer ships a standalone skill for Enchanté (the agent's full capability comes from ``role`` + the ``mcpServers`` MCP tools), so there @@ -240,13 +284,13 @@ def enchante_agent_deeplink() -> str: ``name`` is the **display name** (URL-encoded ``MyKnowledge 知识管理专家``); Enchanté assigns the agent an internal UUID as its key, so ``name`` is not an ID and Chinese text is fine. The base64 is URL-quoted via ``_base64_quote`` - (``+``→``%2B``). Generation only — the actual install capture is handled by - the Enchante client (repeat installs pop a Conflict Resolution float — - Replace / Rename / Skip). + (``+``→``%2B``, ``/``→``%2F``, ``=``→``%3D``). Generation only — the actual + install capture is handled by the Enchante client (repeat installs pop a + Conflict Resolution float — Replace / Rename / Skip). """ import urllib.parse bundle = { - "role": _agent_template(), + "role": _agent_template("Enchante"), "skillNames": [], "mcpServers": { "MyKnowledge": { @@ -272,11 +316,32 @@ def mcp_entry(platform: str) -> dict: identify which client launched it when reporting heartbeats. Each platform gets its own entry (env is per-server, so platforms never overwrite one another). + + Environment-aware command generation: + - **development / PyPI install** (not frozen): ``sys.executable -m + backend.cli mcp`` — the module is located by the installed ``backend`` + package, so the entry survives moving/copying the KB config or an + installed wheel. + - **PyInstaller desktop app** (``sys.frozen``): there is no standalone + python in an onedir bundle, so we reuse the *frozen backend binary + itself* (``sys.executable``) with the ``--mcp`` subcommand. The MCP + ``command``/``args`` are separate JSON list elements that the client + spawns directly (no shell), so a space-containing binary path needs no + quoting — unlike the hooks shell command (``_hooks_command_codebuddy``). + PyInstaller traces the ``from backend.cli import cmd_mcp`` import in + ``desktop_server.py`` into the PYZ bundle, so the binary can run the + MCP server in-process. """ + if getattr(sys, "frozen", False): + cmd = sys.executable # packaged myknowledge-backend binary + args = ["--mcp"] + else: + cmd = sys.executable + args = ["-m", "backend.cli", "mcp"] return { "type": "stdio", - "command": sys.executable, - "args": ["-m", "backend.cli", "mcp"], + "command": cmd, + "args": args, "env": { "MYKNOWLEDGE_ROOT": str(resolve_root()), "MYKNOWLEDGE_CLIENT": platform, @@ -285,14 +350,34 @@ def mcp_entry(platform: str) -> dict: def _hooks_command_claude() -> str: - """Claude/WorkBuddy: curl the hook with $CLAUDE_TOOL_USE_INPUT.""" + """Claude/WorkBuddy: curl the hook, forwarding stdin as the POST body. + + Claude Code writes the PreToolUse JSON payload to the hook command's + **stdin** — there is no ``$CLAUDE_TOOL_USE_INPUT`` env var (that was a + mistaken assumption from an earlier version; confirmed against Claude + Code's hooks docs). A single-quoted ``'$CLAUDE_TOOL_USE_INPUT'`` is never + shell-expanded either way, so the old command always POSTed that literal + string — invalid JSON, so the backend 422'd on *every* call and curl's + non-2xx exit code (0) meant Claude Code silently treated it as "allow". + ``-d @-`` tells curl to read the POST body from stdin instead. + """ return ( f"curl -s -X POST {HOOK_ENDPOINT} " "-H 'Content-Type: application/json' " - "-d '$CLAUDE_TOOL_USE_INPUT'" + "-d @-" ) +# Old, broken Claude command (see _hooks_command_claude docstring) — kept only +# so _matcher_is_mine still recognises an install written before this fix and +# upgrades it in place instead of appending a second, duplicate matcher. +_LEGACY_CLAUDE_HOOK_COMMANDS = ( + f"curl -s -X POST {HOOK_ENDPOINT} " + "-H 'Content-Type: application/json' " + "-d '$CLAUDE_TOOL_USE_INPUT'", +) + + def _hooks_command_codebuddy() -> str: """CodeBuddy: forward stdin JSON via the ``hooks_forward`` helper. @@ -359,9 +444,10 @@ def hooks_matcher(platform: str) -> dict: """The PreToolUse hook matcher (guards bare AI operations via HTTP). Platform-differentiated command: - - ClaudeCode / WorkBuddy: curl with ``$CLAUDE_TOOL_USE_INPUT``, matcher - ``Bash|Write|Edit`` (claude PreToolUse is matched **per tool name**; a bare - ``Bash`` would skip Write/Edit, leaving ``hooks.py``'s file_write branch dead). + - ClaudeCode / WorkBuddy: curl reading stdin (``-d @-``; Claude delivers the + PreToolUse payload on stdin), matcher ``Bash|Write|Edit`` (claude + PreToolUse is matched **per tool name**; a bare ``Bash`` would skip + Write/Edit, leaving ``hooks.py``'s file_write branch dead). - CodeBuddyIDE: helper script reading stdin, matcher ``*`` (match all tools — ``hooks.py`` allows MCP calls internally, so a broad matcher is safe). - Cursor: helper script reading stdin, matcher ``Shell|Write|Delete`` @@ -385,21 +471,24 @@ def hooks_matcher(platform: str) -> dict: def _matcher_is_mine(matcher: dict, cmd: str) -> bool: """True if a PreToolUse matcher is the MyKnowledge hook (by command signature). - Recognises our hook by its command (claude/curl-$CLAUDE_TOOL_USE_INPUT, - codebuddy/hooks_forward.py, or cursor's direct-entry form) so we never - mistake a user's own hook for ours. Handles both the Claude/CodeBuddy - nested ``hooks[0].command`` shape and Cursor's direct ``command`` entry. + Recognises our hook by its command (claude/curl-@-, codebuddy/hooks_forward.py, + or cursor's direct-entry form) so we never mistake a user's own hook for + ours. Handles both the Claude/CodeBuddy nested ``hooks[0].command`` shape + and Cursor's direct ``command`` entry. Also matches + ``_LEGACY_CLAUDE_HOOK_COMMANDS`` so an install written before the + stdin/``-d @-`` fix is upgraded in place rather than duplicated. """ if not isinstance(matcher, dict): return False + recognised = (cmd, *_LEGACY_CLAUDE_HOOK_COMMANDS) # Cursor entries carry the command directly on the entry. - if matcher.get("command") == cmd: + if matcher.get("command") in recognised: return True hooks = matcher.get("hooks") if not isinstance(hooks, list) or not hooks: return False first = hooks[0] - return isinstance(first, dict) and first.get("command") == cmd + return isinstance(first, dict) and first.get("command") in recognised def _merge_hook_entry(matchers: list, entry: dict, cmd: str) -> list: @@ -407,16 +496,16 @@ def _merge_hook_entry(matchers: list, entry: dict, cmd: str) -> list: The matcher is identified by **command signature** (``_matcher_is_mine``), so a user's own hook sharing a matcher string is never touched. If our hook already - exists but its ``matcher`` string is outdated (e.g. the old Claude ``Bash`` that - skipped Write/Edit), we replace it with the current entry instead of leaving the - stale one — otherwise re-running ``write_kind`` on an existing install would keep - the broken matcher and the fix would never take effect. + exists but is stale — an outdated ``matcher`` (e.g. the old Claude ``Bash`` that + skipped Write/Edit) or an outdated ``command`` (e.g. the old + ``$CLAUDE_TOOL_USE_INPUT`` payload bug) — we overwrite it with the current entry + unconditionally. Comparing only ``matcher`` here previously let a stale + ``command`` survive re-installs whenever the matcher string already matched, + so re-running ``write_kind`` silently kept the broken command forever. """ for i, m in enumerate(matchers): if _matcher_is_mine(m, cmd): - if isinstance(m, dict) and m.get("matcher") == entry.get("matcher"): - return matchers # already current — nothing to do - matchers[i] = entry # upgrade stale matcher in place + matchers[i] = entry # upgrade stale matcher/command in place return matchers matchers.append(entry) return matchers @@ -432,16 +521,27 @@ def _agents_dir() -> Path: return _aiclient_config_dir() / "agents" -def _agent_template() -> str: - """Read the agent prompt body from ``backend/AiClientConfig/agents/MyKnowledge-agent.md``. +def _agent_template(platform: str = "") -> str: + """Read the agent prompt body for a platform from ``AiClientConfig/agents/``. Content is separated from code so edits don't require a code change. - Raises a clear ``RuntimeError`` if the template is missing. + + Template selection: + - ``platform == "Enchante"`` → ``MyKnowledge-agent-Enchante.md`` (精简版, + shipped as the deeplink ``role`` — see ``enchante_agent_deeplink``). + - any other / default (``""``) → ``MyKnowledge-agent.md`` (完整版). + + The default (``""``) keeps every existing caller (``agent_content`` for + ClaudeCode/CodeBuddy/WorkBuddy/Cursor, and tests) on the full template + unchanged. Raises a clear ``RuntimeError`` if the selected template is + missing. """ - tpl = _agents_dir() / "MyKnowledge-agent.md" + fname = ("MyKnowledge-agent-Enchante.md" if platform == "Enchante" + else "MyKnowledge-agent.md") + tpl = _agents_dir() / fname if not tpl.is_file(): raise RuntimeError( - f"缺失 Agent 模板: {tpl}(backend/AiClientConfig/agents/MyKnowledge-agent.md)") + f"缺失 Agent 模板: {tpl}(backend/AiClientConfig/agents/{fname})") return tpl.read_text(encoding="utf-8") diff --git a/backend/desktop_server.py b/backend/desktop_server.py index e9b550d..b2e6102 100644 --- a/backend/desktop_server.py +++ b/backend/desktop_server.py @@ -33,6 +33,14 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="myknowledge-desktop-backend") + parser.add_argument( + "subcommand", nargs="?", default=None, choices=["mcp"], + help="alternate form of --mcp — some MCP client integrations " + "(e.g. Enchante configs copied from the pip-installed `myknowledge " + "mcp` docs) spawn the binary with a bare `mcp` positional instead " + "of the `--mcp` flag; accept both so the frozen binary works " + "either way", + ) parser.add_argument( "--port", type=int, default=8080, help="listen port (default 8080)", @@ -45,6 +53,10 @@ def main(argv: list[str] | None = None) -> int: "--hooks-forward", action="store_true", help="run as the CodeBuddy PreToolUse hook forwarder (stdin→hook→stdout)", ) + parser.add_argument( + "--mcp", action="store_true", + help="run as an MCP stdio server (for agent clients; ignores --port)", + ) args = parser.parse_args(argv) if args.hooks_forward: @@ -54,6 +66,19 @@ def main(argv: list[str] | None = None) -> int: # free, and must never block the user when the backend is unreachable. return hooks_forward.main() + if args.mcp or args.subcommand == "mcp": + # MCP stdio mode: reuse cli.cmd_mcp verbatim — it auto-initializes the + # KB (_auto_init), does the identity check, GC, heartbeat reporting and + # app.run(transport="stdio"). --root is None by default here, so + # resolve_root(None) inside cmd_mcp falls through to the + # MYKNOWLEDGE_ROOT env (injected by the client config) then + # ~/.myknowledge — matching the webserver resolution. --port is ignored + # in this mode (stdio has no HTTP). + import argparse as _argparse + ns = _argparse.Namespace(root=args.root) + from backend.cli import cmd_mcp + return cmd_mcp(ns) + kb_root = resolve_root(args.root) os.environ["MYKNOWLEDGE_ROOT"] = str(kb_root) diff --git a/backend/git_manager.py b/backend/git_manager.py index d7dfdac..f510f7d 100644 --- a/backend/git_manager.py +++ b/backend/git_manager.py @@ -42,7 +42,14 @@ def _is_system_noise(path: str) -> bool: class GitManager: - """Wrap ``git`` operations for a knowledge base directory.""" + """Wrap ``git`` operations for a knowledge base directory. + + Args: + repo_root: the git repository root **as a ``Path``** (usually + ``storage.kb_root``), NOT a ``Storage`` instance. Passing a + ``Storage`` object here raises ``AttributeError`` (it has no + ``resolve``). + """ def __init__(self, repo_root: Path) -> None: self.repo = repo_root.resolve() diff --git a/backend/main.py b/backend/main.py index d1f78e3..fa1df3b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1271,6 +1271,18 @@ def api_client_config_detect(): return detect_all() +@app.get("/api/platforms-meta") +def api_platforms_meta(): + """Return enabled platforms' display metadata. + + ``{ClaudeCode: {"display": "Claude Code", "enabled": true}, ...}`` — + 供前端把展示名改为从后端配置读取(企业定制 display 后前端无需改代码)。 + 只含 enabled 平台;结构独立于 /api/client-config(检测状态),互不影响。 + """ + from backend.client_config import platforms_meta + return platforms_meta() + + @app.post("/api/mcp/heartbeat") def api_mcp_heartbeat(request: Request): """MCP server liveness report. diff --git a/backend/mcp_server.py b/backend/mcp_server.py index d032edc..dda8640 100644 --- a/backend/mcp_server.py +++ b/backend/mcp_server.py @@ -1234,6 +1234,15 @@ def create_mcp_app(storage: Storage, storage: ``Storage`` instance for the KB. gen: ``ReadmeGenerator`` (enables write-through). gm: ``GitManager`` (enables diff & checkpoint tools). + + Direct-invocation contract (for tests / ad-hoc scripts, NOT the MCP wire): + ``await app.call_tool(name, args)`` returns a **tuple** + ``(list_of_TextContent, meta_dict)``. The human-readable text of the + first result is ``result[0][0].text`` (a ``TextContent``). Helper + pattern used across tests:: + + result = asyncio.run(app.call_tool("maint__check_uncommitted", {})) + text = result[0][0].text """ mcp = FastMCP("MyKnowledge") diff --git a/backend/storage.py b/backend/storage.py index 058fe68..0b9e123 100644 --- a/backend/storage.py +++ b/backend/storage.py @@ -126,7 +126,14 @@ class ReadmeMeta: # ══════════════════════════════════════════════════════════════ class Storage: - """File operations bound to a specific knowledge base root.""" + """File operations bound to a specific knowledge base root. + + Args: + kb_root: the knowledge base root **as a ``Path``** (not a str — it is + resolved via ``Path.resolve()``). A plain ``str`` raises + ``AttributeError``. Use ``storage.kb_root`` (a Path) when passing + the root elsewhere, e.g. ``GitManager(repo_root=storage.kb_root)``. + """ def __init__(self, kb_root: Path, templates_dir: Optional[Path] = None) -> None: self.kb_root = kb_root.resolve() diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index c079ae8..35afd80 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -1,6 +1,6 @@ # MyKnowledge 桌面壳 — electron-builder 配置 -# 产出:MyKnowledge-0.7.5-arm64.dmg / .zip(Apple Silicon) -# MyKnowledge-0.7.5-x64.dmg / .zip(Intel) +# 产出:MyKnowledge-0.7.7-arm64.dmg / .zip(Apple Silicon) +# MyKnowledge-0.7.7-x64.dmg / .zip(Intel) appId: com.myknowledge.desktop productName: MyKnowledge diff --git a/desktop/enterprises/README.md b/desktop/enterprises/README.md new file mode 100644 index 0000000..90de5db --- /dev/null +++ b/desktop/enterprises/README.md @@ -0,0 +1,54 @@ +# 企业定制配置 + +每个 JSON 文件代表一个企业的 dmg 定制配置。文件名为企业名(如 `yourcompany.json` → `--enterprise yourcompany`)。 + +> **配置文件不进代码仓库**:`desktop/enterprises/*.json`(除 `template.json` 外)已被 `.gitignore` 忽略。 +> 使用方式:复制 `template.json` 为 `<企业名>.json` 再修改,该文件只存在于本地/分发机,不会提交。 + +## 打包用法 + +```bash +# 默认(全平台,不读企业配置) +bash scripts/release.sh + +# 企业定制 +bash scripts/release.sh --enterprise yourcompany +``` + +## 配置格式 + +```json +{ + "name": "yourcompany", + "default_disabled": false, + "platforms": { + "ClaudeCode": { "enabled": true, "display": "Claude Code", "order": 1 }, + "WorkBuddy": { "enabled": false } + } +} +``` + +- `name`:企业名(须与文件名一致,便于排查) +- `default_disabled`:`false`(缺省)→ 未列出的平台沿用 `platforms.json` 默认;`true` → 未列出的平台**全部禁用**(只启用显式列出的,适合"仅开放少数平台"场景) +- `platforms`:只写需要覆盖的平台;未列出的平台行为由 `default_disabled` 决定 +- `enabled`:`false` → 该平台不出现在产物(前端列表、检测、配置生成均不含) +- `display`:覆盖该平台的展示名(前端从 `/api/platforms-meta` 读取,无需改前端代码) +- `order`:控制前端展示顺序(数字小的靠前;未配置的平台排在后面保持原序) +- `kinds`:覆盖该平台支持的能力集(如 `["mcp", "agent"]`);前端 MCP/Hooks/Agent 三页根据它判定"平台原生不支持"置灰 +- **平台 key 必须与 `platforms.json` 的 key 严格一致**,否则打包报错 + +## 常用场景 + +| 场景 | 配置 | +|---|---| +| 只给某企业开放少数平台 | `"default_disabled": true` + 显式列出要启用的平台 | +| 平台改名(品牌定制) | 指定平台的 `display` | +| 禁用某平台 | 该平台 `enabled: false` | +| 调整展示顺序 | 指定平台的 `order` | +| 声明平台不支持某能力 | 覆盖该平台的 `kinds`(MCP/Hooks/Agent 页置灰显示) | + +## 注意 + +- 企业配置**只影响打包产物**,`backend/AiClientConfig/platforms.json` 源文件不被修改(临时合并,打包后清理) +- 当前是**打包期合并**:一个企业 = 一份配置 + 一次打包 = 一个 dmg +- 平台 key 全集见 `backend/AiClientConfig/platforms.json`:`ClaudeCode` / `ClaudeDesktop` / `CodeBuddyIDE` / `WorkBuddy` / `Enchante` / `Cursor` diff --git a/desktop/enterprises/template.json b/desktop/enterprises/template.json new file mode 100644 index 0000000..9f06dbe --- /dev/null +++ b/desktop/enterprises/template.json @@ -0,0 +1,13 @@ +{ + "_comment": "企业定制配置模板。复制本文件为 <企业名>.json(如 Apple.json),再按需修改。platforms 键与 backend/AiClientConfig/platforms.json 的平台 key 严格一致。语义:default_disabled 缺省 false → 未列出的平台沿用 platforms.json 默认(enabled=true + 默认 display,即不写=启用);default_disabled=true → 未列出的平台全部禁用(只启用显式列出的)。enabled=false 的平台不会出现在产物。可选字段:display 覆盖展示名、order 控制前端顺序(数字小靠前)、kinds 覆盖平台支持的能力集(如 [\"mcp\",\"agent\"],MCP/Hooks/Agent 三页据此置灰「平台原生不支持」)。注意:本文件是模板,仅作参考;desktop/enterprises/*.json(除 template.json 外)已被 .gitignore 忽略,不会进入代码仓库。", + "name": "YourCompany", + "platforms": { + "ClaudeCode": { + "enabled": true, + "display": "Claude Code" + }, + "WorkBuddy": { + "enabled": false + } + } +} diff --git a/desktop/loading.html b/desktop/loading.html index bc80b82..8ae5c73 100644 --- a/desktop/loading.html +++ b/desktop/loading.html @@ -93,11 +93,11 @@ animation: splash-text-in 0.3s ease both; } /* 文本延迟到绿条展开(height 3px→26px,0.6s)完成后再显示, - 避免在低高度下被挤压/错位 */ + 避免在低高度下被挤压/错位。仅 animation-delay 推迟开始; + 不另设 opacity:0(与 animation: ... both 冲突会覆盖 to 态 → 文本永久不可见) */ .splash__bar-fill--done .splash__bar-text { display: block; animation-delay: 0.55s; - opacity: 0; } @keyframes splash-text-in { from { opacity: 0; transform: translateY(4px); } @@ -116,7 +116,7 @@ - + + - + @@ -86,6 +86,36 @@ $store.app.init(); " > +
知识健康检查
- +
- +
- + - + - + - + - + - + - + @@ -1808,7 +1841,7 @@ - +