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/.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..158c806 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,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..44684ac 100644 --- a/README.zh.md +++ b/README.zh.md @@ -137,7 +137,7 @@ pytest tests/ -v ## 版本 -- **系统版本**:定义在 `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/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..e2e155a 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: @@ -224,11 +266,19 @@ def enchante_agent_deeplink() -> str: ``enchante://agent/install?name=MyKnowledge 知识管理专家&config=`` Creates a one-click dedicated "MyKnowledge 知识管理专家" role in Enchanté's top - Agent dropdown. Payload schema confirmed with Enchante (2026-08-19): + Agent dropdown. Payload schema confirmed with Enchante (2026-08-19), amended + 2026-08-26 after a real-device install failed silently — Enchanté requires + the display name **inside** the base64 JSON too (top-level ``name``), not + only as the outer ``?name=`` query param; without it, deserialization/ + validation fails silently and the deeplink never surfaces an install prompt: + - ``name``: same display string as the outer query param, duplicated at + the JSON top level (Enchanté's requirement, not redundant on our side). - ``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 +290,15 @@ 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 + display_name = "MyKnowledge 知识管理专家" bundle = { - "role": _agent_template(), + "name": display_name, + "role": _agent_template("Enchante"), "skillNames": [], "mcpServers": { "MyKnowledge": { @@ -257,7 +309,7 @@ def enchante_agent_deeplink() -> str: } }, } - name = urllib.parse.quote("MyKnowledge 知识管理专家") + name = urllib.parse.quote(display_name) return (f"enchante://agent/install?name={name}&config=" f"{_base64_quote(bundle)}") @@ -272,11 +324,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 +358,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 +452,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 +479,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 +504,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 +529,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/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/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 @@ - + - + @@ -1733,11 +1733,14 @@ x-show="!$store.app.clientInstalled('Enchante')">请先安装 Enchanté - + @@ -1980,7 +1983,8 @@