Compare commits

..

10 Commits

Author SHA1 Message Date
lanrtop
1888f71626 docs(blueprint/agent-client-parity): P1 收尾审查记录与已知限制更新为 claude mcp get 为准的方案
All checks were successful
Push & PR Check / check (push) Successful in 1m32s
Push & PR Check / check (pull_request) Successful in 1m5s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:57:26 +09:00
lanrtop
0a92a5b695 fix(mcp/parity): 收尾审查修复——Claude 现状以 claude mcp get 为准,接入命令移出界面线程
收尾对抗审查指出:只读扫描 ~/.claude.json 的结论不能代表 Claude 在项目目录实际生效的条目
(共享项目键、worktree、目录迁移下会误删他项目条目或漏写),且同步 CLI 调用会阻塞界面线程。

- 快路径只认「项目根或其祖先目录键下」的一致托管条目;其余一律在项目目录执行
  `claude mcp get lian-jing` 取实际生效条目,按归属决策:本项目托管条目 → 刷新/移除
  (写入失败用原条目回滚);他项目托管条目或手写条目 → 不碰并记录;无 local 条目 → 写入
- 输出无法识别、CLI 出错或目录不存在时一律不写,只记 warn
- 全局迁移需 claude CLI 与 git-bash 均可用才置完成标记;批量重注入的成员复查失败计入错误
- 删组/移出组先删成员关系再清 MCP 配置,避免并发重注入把项目加回;调 CLI 前释放数据库连接
- delete_group / add_group_member / remove_group_member / delete_project / update_settings /
  apply_blueprint_buff 改为 #[tauri::command(async)],在线程池执行
- 新增 parse_claude_mcp_get / lianjing_project_of / key_covers_root 单元测试

实测(enterprise):稳态注入 174ms 不起 CLI;条目缺失 → 写入;过期 helper → 刷新;
手写条目 → 保留;三种情况 claude mcp get 均为 Local 且 Connected;经 shim 零凭据探针认证通过。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:55:28 +09:00
lanrtop
92e6450403 docs(blueprint/agent-client-parity): 更新 P1 审查记录与已知限制为最终方案
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:41:08 +09:00
lanrtop
5f6ea586f8 fix(mcp/parity): 最终审查修复——Claude 条目写入回归 claude CLI,只读扫描认领
最终审查指出直接编辑 ~/.claude.json 须复刻 Claude 私有机制(git 根 / worktree→主仓库 /
NFC 键推导、.claude.json.lock 锁协议、CLAUDE_CONFIG_DIR 与旧版 .config.json、BOM/符号链接/
fsync/键顺序),随 Claude 升级易失效,违背「标准优先、私有机制只进适配层」。

- 写入一律经 claude CLI(cwd=项目目录,同步、带 20 秒超时),由 Claude 自己负责键、锁与路径
- 只读扫描按 URL 中 /mcp/projects/<id> 认领炼境托管条目(不依赖键推导);已一致则不起 CLI
  (实测稳态注入 0.04 秒),手写条目不碰;项目目录已不存在时只记录残留、不写文件
- 全局注册只读确认已存在就跳过 CLI;配置路径按 Claude 规则解析,容忍 BOM
- 批量重注入在锁内复查组成员关系,已移出组的项目不再注入、不重建 token

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:38:36 +09:00
lanrtop
1f90dcdeb0 docs(blueprint/agent-client-parity): P1/P3 完成,记录三端验收与审查证据
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:44:00 +09:00
lanrtop
fc8492b88b fix(mcp/parity): 第三轮审查修复——Claude 条目改为直接编辑配置,接入操作加进程锁
- Claude local 条目不再经 claude CLI(cwd=项目目录)+ 后台队列管理,改为直接编辑
  ~/.claude.json 中本项目键下的 mcpServers.lian-jing:同步、毫秒级、原子替换,只动该字段;
  不依赖项目目录存在,消除删除本地文件夹时的竞态、目录移动后旧条目无法清理、队列丢任务/卡死
- 项目键用真实长路径(实测 8.3 短路径键不被 Claude 识别);手写条目不碰
- 注入、移除、根目录迁移在进程锁内串行;移除时最先删 token,后续清理收集错误统一返回
- 根目录迁移:旧根解析不出也迁移;重新注入失败归入 mcp_inject 并补刷 DSH;DSH 补丁写入加锁

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:37:37 +09:00
lanrtop
7b702a9d88 docs(blueprint): 新建 architecture-evolution 模块,记录技术栈评估与 A1–A4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:49:59 +09:00
lanrtop
e4214cd839 fix(mcp/parity): 第二轮审查修复——串行同步队列、白名单持久剔除、诊断分类
- Claude local 同步统一投递到单个后台串行队列(注册与移除同队列、按提交顺序),
  消除并发 claude CLI 竞态与「注入后立即移除」留下孤儿条目;移除路径不再阻塞界面
- DSH 白名单只收有项目 token 的项目,退组/移除后剔除持久;控制面项目写 null 停止哨兵,
  不再越过它匹配到已登记的上级目录;Rust 与 JS 两端键规范化对齐(重复分隔符、盘符根、UNC)
- 客户端侧同步日志改为 mcp_client_sync 分类,不覆盖诊断中每个项目的注入结果
- 批量重注入只在末尾刷新一次 DSH;启动刷新移到后台线程
- 项目根目录/平台变更时迁移客户端配置(清旧根 Claude 条目,已接入项目在新根重新注入)
- SSE 消息路由仅在会话属于该项目时才移除;注入模块的进程调用统一 silent_cmd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:39:23 +09:00
lanrtop
0bf5bd6994 docs(blueprint/agent-client-parity): 立卡 P8 一键接入
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:58:09 +09:00
lanrtop
0c40e1a5c9 fix(mcp/parity): 对抗审查 9 项修复——控制面保留全局、DSH 改登记白名单
- 控制面项目(context.json mcp_scope=global,炼境自身已声明)不写 Claude local 条目、
  不入 DSH 白名单,避免遮蔽全局注册与跨项目工具(审查 high)
- DSH 托管块内嵌炼境登记的「项目根目录→project_id」白名单,不再以任意目录里的
  context.json 作为取 token 依据;LOCALAPPDATA/SystemRoot 须为绝对路径
- remove_project_mcp 同步清理 Claude local 条目并剔除 DSH 白名单;SSE 消息路由复核登记
- Claude local 只替换炼境托管条目,手写不覆盖,替换失败回滚;改走 silent_cmd 并移至后台线程
- 手写 DSH 条目改用 serde_yaml 检测(引号/注释/流式写法),无法解析时保守拒绝
- shim 中 exe 路径的 % 转义为 %%;注入日志 context 统一为 JSON,诊断查询防御非 JSON 行

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:48:41 +09:00
13 changed files with 1058 additions and 193 deletions

View File

@ -6,7 +6,7 @@ description: >
项目自身保留业务规则、测试、部署声明与交付决策。炼境本身亦被纳入其中,
形成可自我进化的开发智能闭环。
iteration: 1
updated: 2026-09-30
updated: 2026-10-01
agent_button_carrier: tauri
@ -511,11 +511,20 @@ modules:
name: 三端客户端对等
area: infra
status: in_progress
progress: 0
updated: 2026-09-30
note: "目标:子项目在 Claude Code、Codex、DSH 三端都能继续开发。2026-09-30 无头基线探针:Codex 全绿;Claude 与 DSH 连全局 /mcp、看不到开发变量工具;Claude 缺 lian-jing skill。先统一连接地址,再处理认证与 skill 投影,并把探针固化为可重跑检查。"
progress: 38
updated: 2026-10-01
note: "P1/P2/P3 完成:三端(Claude Code、Codex、DSH)在 Enterprise 经稳定 shim 连项目专用地址,无头验收 B–F 全部通过(基线时 Claude、DSH 缺工具与认证、Claude 缺 skill)。Codex 项目配置不含本机路径;Claude 条目直接编辑 ~/.claude.json;DSH 托管块按登记白名单路由。四轮对抗审查问题均已修复。待做 P4 探针固化(依赖 architecture-evolution A1)、P5–P8。已知限制:DSH 桌面 GUI 模式回退全局。"
position: [3500, 950]
- id: architecture-evolution
name: 炼境架构演进
area: infra
status: planned
progress: 0
updated: 2026-10-01
note: "技术栈评估:Tauri v2 + Rust + React/TS 适合管家方针,不换栈。按收益排序的结构改进:A1 本体与投影分离(core crate + CLI,P4/P8 依赖)、A2 前后端类型契约生成、A3 配置文本模板化、A4 MCP 改用官方 rmcp SDK。"
position: [3750, 950]
edges:
- from: project-resource-broker
to: project-dev-variables
@ -854,3 +863,7 @@ edges:
- from: project-dev-variables
to: agent-client-parity
type: dependency
# P4 探针固化、P8 一键接入以本体与投影分离(A1 的 CLI 投影)为承载。
- from: architecture-evolution
to: agent-client-parity
type: dependency

View File

@ -80,13 +80,16 @@ skill 只是五个接入面之一,兼容层覆盖全部五面。
## 任务拆解
### 🔵 P1 — 统一连接项目专用地址(Claude + DSH)
- status: in_progress
### ✅ P1 — 统一连接项目专用地址(Claude + DSH)
- status: done
- complexity: M
- files: src-tauri/src/mcp_inject.rs, docs/ai-context/agent-button-system.md
- depends: H1, H2
- acceptance: "agent_verify: [三端无头探针] 在 enterprise 目录 Claude 与 DSH 的 C 项均看到开发变量工具(项目工具集),E 项成功;项目目录外的会话不获得他项目工具。"
- 注:吸收原 project-dev-variables V17(Claude 以 local scope + headersHelper 取值)。
- verification: "2026-10-01 Enterprise 三端无头验收(零凭据:F 项以 read_project_test_account(name=LJ_PROBE_NONEXISTENT) 判定认证,返回「开发需求未登记」即认证通过):Codex 59s、Claude 40~56s、DSH 11~14s,三端 B/C/D/E/F 全部通过(基线时 Claude、DSH 的 C/E/F 失败、Claude 的 B 失败)。Claude 以 init 事件客观确认 lian-jing 32 个项目工具且含开发变量工具;claude mcp list 显示项目地址 ✔ Connected。DSH 托管块 url 表达式在项目根、子目录、带尾分隔符路径解析为项目地址,在上级/无关目录回退全局;控制面 null 哨兵命中即停。未知 project_id 返回 404。"
- review: "六轮对抗审查(首轮 9 项、第二轮 13 条、第三轮 10 条、最终 10 条、收尾 13 条,均已处理)。Claude 条目写入方式三度调整:CLI+后台队列(cwd 依赖与竞态)→ 直接编辑 ~/.claude.json(须复刻 Claude 的 git 根/worktree/NFC 键推导、.claude.json.lock、CLAUDE_CONFIG_DIR 等私有机制)→ 只读扫描认领 + CLI 写入(扫描结论不代表本目录实际生效条目,共享键下会误删他项目)→ 最终定为『Claude 自己的回答为准』:快路径只认项目根或其祖先键下的一致托管条目,其余在项目目录执行 claude mcp get 取实际生效条目,按归属(本项目托管/他项目托管/手写/无)决策,写入经 claude CLI、失败回滚;涉及 CLI 的 Tauri 命令改到线程池执行,删组/移出组先删成员关系再清配置。实测(enterprise):稳态注入 174ms 不起 CLI;缺失 → 写入、过期 helper → 刷新、手写条目 → 保留,claude mcp get 均为 Local 且 ✔ Connected;经 shim 零凭据探针认证通过。"
- known_limits: "DSH 桌面 GUI 模式的 cwd 不随工作区切换,项目路由目前只对在项目目录启动的 headless/CLI 生效,GUI 回退全局;claude mcp list 会提示 lian-jing 同时存在 user 与 local 两个作用域(user 级全局为有意保留的兜底);项目目录已被移动或删除后,Claude 中的残留托管条目无法经 CLI 定位,只记录 warn 与手工清理命令,不直接改写 ~/.claude.json;共享 Claude 项目键的嵌套项目/worktree 只有键所属项目的条目生效,他项目不改动、只记 warn;非稳态同步依赖 claude mcp get 的人类可读输出(格式无法识别时按未知处理、不写入),Claude 改输出格式会退化为只告警。"
### ✅ P2 — Claude skill 投影
- status: done
@ -96,13 +99,14 @@ skill 只是五个接入面之一,兼容层覆盖全部五面。
- acceptance: "agent_verify: [cargo test onboarding] 接入包同时写 .claude/skills/lian-jing/SKILL.md,内容与 .agents/skills 单一来源一致;[Claude 无头探针] B 项看到 lian-jing。"
- verification: "2026-10-01:同一模板双投影到 .agents/skills 与 .claude/skills;verify_onboarding 新增「Claude 炼境技能已分发」(检查链 12 项);base-onboarding 升至 1.6.0,supported_clients 增 dsh;skill 降级步骤改为三端通用并提示「无开发变量工具即连的是全局地址」。新增测试 skill_is_projected_to_codex_and_claude_from_one_source;cargo test --locked 229 passed / 5 ignored。Enterprise 分发后 verify_onboarding 14/14,两份 skill 逐字节一致;Claude 无头会话 init 事件 skills 与 slash_commands 均含 lian-jing(基线探针时为不存在)。"
### 🔵 P3 — 稳定 helper 入口,项目配置去本机路径
- status: in_progress
### ✅ P3 — 稳定 helper 入口,项目配置去本机路径
- status: done
- complexity: M
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/lib.rs, src-tauri/src/commands/onboarding.rs
- depends: H4
- acceptance: "agent_verify: [cargo test] 炼境启动写/更新 %LOCALAPPDATA% 下 shim 指向当前 exe;inject 后受跟踪文件不含本机绝对路径或用户名;[Codex 无头探针] F 项经 shim 成功;换构建目录启动后无需重注入仍成功。"
- 注:即原 project-dev-variables V18;Claude headersHelper 与 DSH 共用同一 shim。
- verification: "2026-10-01:shim 写入 %LOCALAPPDATA%\lian-jing\bin\lianjing-mcp-header.cmd(CRLF,exe 路径 % 转义),Enterprise 的 .codex/config.toml 托管块 helper 为 %LOCALAPPDATA% 形式、不含本机路径与用户名;Codex 无头 F 项经 shim 认证通过。H4 实测证实 Codex 经 cmd 展开 %LOCALAPPDATA%(替身目录因果对照)。换构建目录启动后免重注入:shim 每次启动重写指向当前 exe(实测在 worktree 构建启动时自动改指向)。"
### 📋 P4 — 三端对等探针固化为炼境能力
- status: todo
@ -132,3 +136,11 @@ skill 只是五个接入面之一,兼容层覆盖全部五面。
- depends: P1
- acceptance: "agent_verify: [三端无头探针] 在 initialize 下发唯一标记的 instructions,判定三端是否把它带入模型上下文。全部支持则把引导提示迁到服务器说明,本地 skill 只保留 MCP 不可达时的排障步骤;部分支持则如实记录、保留 skill 引导。"
- 为什么:Claude 已实证会把 lian-jing 的服务器说明带入上下文;若三端皆支持,引导提示可由炼境集中维护。
### 📋 P8 — 一键接入:创建/添加子项目即完成三端可用
- status: todo
- complexity: M
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/commands/onboarding.rs, src-tauri/src/commands/projects.rs(实现时定)
- depends: P4
- acceptance: "agent_verify: [Tauri command / MCP] 对新登记项目执行一次接入 → 依次完成 canonical context、接入包分发(规则、skill 双投影、Codex 托管块)、本机三端注册(Claude local、DSH 白名单、shim、token),并以三端无头探针给出 A–F 兼容矩阵;重复执行幂等;接入包对项目文件的改动只报告 diff、不自动提交。"
- 为什么:2026-10-01 Enterprise 验收时,接入散落在 inject_mcp、apply_onboarding_pack、verify_onboarding 与手工探针多个步骤。按「分发 vs 查询」:随仓库走的文件由炼境分发一次、按版本升级;与机器相关的客户端注册由炼境在本机维护、不进仓库;易变信息走 MCP 查询。默认三端都接入(成本小,保留随时换客户端的能力);能用标准的不做专门适配(Codex、DSH 原生读 AGENTS.md 与 .agents/skills,只有 Claude 需要 CLAUDE.md 与 .claude/skills 投影);炼境离线时规则与 skill 仍可用。

View File

@ -0,0 +1,45 @@
---
id: architecture-evolution
name: 炼境架构演进
status: planned
progress: 0
---
## 概述
2026-10-01 评估结论:现有技术栈(Tauri v2 + Rust + React/TS + SQLite)适合「管家 / agent 客户端兼容层」方针,不换栈。常驻托盘服务、凭据安全、单文件分发、cargo test 快速回归都是 Rust/Tauri 的强项;真正的难点在 Windows 与各客户端自身机制(cmd 引号与 %VAR% 展开、CRLF、Claude 配置作用域、DSH !!js、Node 拒绝直接执行 .cmd),与语言无关。Electron 全 Node 栈(与三家客户端同生态、JSON/YAML 顺手)因常驻内存大、凭据需原生模块且须整体重写而否决;Go、Python 无明显收益。
本模块收集比「换语言」更影响实现效率的结构性改进,按收益排序。
## 任务拆解
### 📋 A1 — 本体与投影分离:core crate + CLI
- status: todo
- complexity: L(先 /architect 再拆)
- files: src-tauri/Cargo.toml(改为 workspace)、新增 core 与 cli crate(实现时定)
- acceptance: "agent_verify: [cargo test] 业务逻辑位于不依赖 Tauri 的 core crate;[CLI] 同一 exe 或独立 cli 在炼境界面未运行时可执行 onboard / probe / diagnostics 等命令并返回结构化结果。"
- 为什么:核心逻辑写在 Tauri 应用内,炼境不运行时 agent 与脚本无法调用。业务函数已按「连接由调用方注入」编写,拆出成本可控。拆分后 Tauri 界面、MCP 服务、命令行成为同一本体的三个投影,契合「一个本体 + 多个投影」原则;agent-client-parity 的 P4 探针、P8 一键接入可直接做成命令。shim 已在用 exe 的命令行模式(--lianjing-mcp-header),只是尚未系统化。
### 📋 A2 — 前后端类型契约自动生成
- status: todo
- complexity: M
- files: src-tauri/src/commands/、src/lib/commands.ts
- acceptance: "agent_verify: [pnpm typecheck] commands.ts 的参数与返回类型由 Rust 定义生成(tauri-specta 或 ts-rs),新增 command 不再手抄类型;删改 Rust 字段时 typecheck 报出所有受影响处。"
- 为什么:现由规则 R01 要求手动同步 commands.ts,易漏;对应四支柱「可推理」。
### 📋 A3 — 配置文本生成模板化
- status: todo
- complexity: M
- files: src-tauri/src/mcp_inject.rs、src-tauri/resources/
- acceptance: "agent_verify: [cargo test] Codex 托管块、DSH 托管块、shim 等由模板(如 minijinja)渲染,并有「渲染结果是合法 YAML/TOML/JSON」测试。"
- 为什么:2026-10-01 实现 P1 时在 Rust 中手拼 YAML/JS,出现缩进被字符串续行吞掉、转义少一个反斜杠两个缺陷(均被测试拦下)。模板能从写法上避免这类问题。
### 📋 A4 — MCP 协议改用官方 Rust SDK(rmcp)
- status: todo
- complexity: M
- files: src-tauri/src/mcp_server.rs、src-tauri/src/mcp_tools.rs
- acceptance: "agent_verify: [cargo test + 三端无头探针] 换用 rmcp 后 scoped 路由、敏感读取认证、SSE 与 streamable HTTP 行为不变,三端 A–F 矩阵不回退。"
- 为什么:现为手写 JSON-RPC + SSE。各客户端对 MCP 的支持持续演进(streamable HTTP、认证、服务器说明等),自维护协议细节的成本会上升。
### 附:调用外部进程的 Tauri command 统一 async
- 同步 Tauri command 跑在主线程,内部调用 claude CLI 等外部进程会卡界面;agent-client-parity 审查已两次发现此类问题。新增此类 command 默认 async,存量随相关改动迁移,不单独立卡。

View File

@ -1,4 +1,5 @@
{
"project_id": "dev-manager-tauri",
"schema_version": 1
"schema_version": 1,
"mcp_scope": "global"
}

View File

@ -6,7 +6,7 @@ contracts:
- label: API 接口(Axum)
paths:
- "src-tauri/src/mcp_server.rs"
doc: "MCP 仅监听 127.0.0.1;新项目使用 /mcp/projects/<project_id> scoped route,连接 scope 拒绝跨项目参数和全局管理写工具。scoped route 只服务已登记项目,未知 project_id 返回 404(数据库不可用同样拒绝)。各客户端经 %LOCALAPPDATA%/lian-jing/bin/lianjing-mcp-header.cmd 稳定入口取认证头(炼境每次启动指向当前 exe):Codex 写项目 .codex/config.toml,Claude 写 ~/.claude.json 的 local 条目(优先于 user 级全局),DSH 写 ~/.dsh/cordis.patch.yml 托管块(!!js 按启动目录选项目地址,项目外回退全局)。旧 /mcp 只作兼容迁移。read_project_dev_variable 与 read_project_test_account 是两种有界开发值读取:仅 scoped 路由、匹配项目的本机 bearer 认证后单项读取;认证失败在 dispatch 前拒绝。路由变更需同步 Agent 按钮体系文档。"
doc: "MCP 仅监听 127.0.0.1;新项目使用 /mcp/projects/<project_id> scoped route,连接 scope 拒绝跨项目参数和全局管理写工具。scoped route 只服务已登记项目,未知 project_id 返回 404(数据库不可用同样拒绝)。各客户端经 %LOCALAPPDATA%/lian-jing/bin/lianjing-mcp-header.cmd 稳定入口取认证头(炼境每次启动指向当前 exe):Codex 写项目 .codex/config.toml,Claude 写 local 条目(优先于 user 级全局;一律经 claude CLI、cwd=项目目录写入,项目键推导、文件锁与配置路径交给 Claude 自身,不直接编辑 ~/.claude.json;项目根或其祖先键下已有一致的托管条目则不起 CLI,否则以项目目录下 claude mcp get 的实际生效条目为准:本项目托管条目刷新/移除,他项目托管条目与手写条目不碰,无法确认现状一律不写;项目目录已不存在时只记录残留不写入),DSH 写 ~/.dsh/cordis.patch.yml 托管块(内嵌炼境登记的「项目根目录→project_id」白名单,!!js 从启动目录向上精确匹配根目录才选项目地址并取认证,任意目录里的 context.json 不作授权依据;未命中回退全局;项目增删与端口变更时重写)。Claude local 条目与 DSH 块只替换炼境托管的条目,手写同名配置不覆盖;移除项目时一并清理。旧 /mcp 只作兼容迁移。read_project_dev_variable 与 read_project_test_account 是两种有界开发值读取:仅 scoped 路由、匹配项目的本机 bearer 认证后单项读取;认证失败在 dispatch 前拒绝。路由变更需同步 Agent 按钮体系文档。"
- label: MCP 工具接口
paths:
@ -25,7 +25,7 @@ contracts:
- ".lian-jing/context.json"
- ".lian-jing/capability-packs.json"
- ".claude/liangjing.json"
doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。lian-jing skill 以同一模板双投影:`.agents/skills/lian-jing/`(Codex、DSH 读取)与 `.claude/skills/lian-jing/`(Claude Code 不读 .agents/skills),两份内容必须一致。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。"
doc: "`.lian-jing/context.json` 是 project_id 的工具无关单一事实来源;`.claude/liangjing.json` 仅为兼容投影;`.lian-jing/capability-packs.json` 是炼境分发基线的版本描述。lian-jing skill 以同一模板双投影:`.agents/skills/lian-jing/`(Codex、DSH 读取)与 `.claude/skills/lian-jing/`(Claude Code 不读 .agents/skills),两份内容必须一致。context.json 可选字段 mcp_scope=global 声明控制面项目(如炼境自身):Claude 不写 local 条目、DSH 不入白名单,保留全局地址与跨项目工具。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。"
- label: Tauri command 接口(前后端契约)
paths:

View File

@ -17,7 +17,8 @@ pub struct BuffStatus {
// ── Tauri 命令 ───────────────────────────────────────────────────────────────
/// 向项目施加蓝图 Buff:注入 CONVENTIONS + CLAUDE.md,写入 DB
#[tauri::command]
// 会经 claude CLI 写客户端配置:在线程池执行,不占界面线程
#[tauri::command(async)]
pub fn apply_blueprint_buff(
project_path: String,
project_id: String,

View File

@ -72,10 +72,10 @@ pub fn update_group(id: String, input: GroupInput) -> Result<(), String> {
Ok(())
}
#[tauri::command]
// 会经 claude CLI 清理客户端配置:在线程池执行,不占界面线程
#[tauri::command(async)]
pub fn delete_group(id: String) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 先清除所有成员的 MCP 配置(DB cascade 删除之前)
let member_ids: Vec<String> = conn
.prepare("SELECT project_id FROM project_group_map WHERE group_id = ?1")
.map_err(|e| e.to_string())?
@ -83,18 +83,24 @@ pub fn delete_group(id: String) -> Result<(), String> {
.map_err(|e| e.to_string())?
.filter_map(|r| r.ok())
.collect();
for project_id in &member_ids {
// 单一 lian-jing key 为全项目共用:成员还属于其他组时保留配置
let in_other_group: i64 = conn
.query_row(
// 单一 lian-jing key 为全项目共用:成员还属于其他组时保留配置。删组前先算出需清理的成员
let to_clear: Vec<&String> = member_ids
.iter()
.filter(|project_id| {
conn.query_row(
"SELECT COUNT(*) FROM project_group_map WHERE project_id = ?1 AND group_id != ?2",
params![project_id, id],
|row| row.get(0),
|row| row.get::<_, i64>(0),
)
.unwrap_or(0);
if in_other_group > 0 {
continue;
}
.unwrap_or(0)
== 0
})
.collect();
// 先删组(级联删除成员关系)再清 MCP 配置:批量重注入在锁内复查成员关系,先删可避免把它们重新注入
conn.execute("DELETE FROM product_groups WHERE id = ?1", params![id])
.map_err(|e| e.to_string())?;
drop(conn);
for project_id in to_clear {
if let Err(e) = mcp_inject::remove_project_mcp(project_id) {
let ctx = serde_json::json!({"project_id": project_id, "group_id": id}).to_string();
log_event(
@ -105,8 +111,6 @@ pub fn delete_group(id: String) -> Result<(), String> {
);
}
}
conn.execute("DELETE FROM product_groups WHERE id = ?1", params![id])
.map_err(|e| e.to_string())?;
Ok(())
}
@ -181,6 +185,7 @@ pub fn ensure_standalone_group(project_id: &str) -> Result<(), String> {
params![project_id, group_id],
)
.map_err(|e| e.to_string())?;
drop(conn);
if let Err(e) = mcp_inject::inject_project_mcp(project_id) {
let ctx = serde_json::json!({"project_id": project_id, "group_id": &group_id}).to_string();
@ -195,7 +200,8 @@ pub fn ensure_standalone_group(project_id: &str) -> Result<(), String> {
Ok(())
}
#[tauri::command]
// 会经 claude CLI 写客户端配置:在线程池执行,不占界面线程
#[tauri::command(async)]
pub fn add_group_member(
project_id: String,
group_id: String,
@ -207,6 +213,7 @@ pub fn add_group_member(
params![project_id, group_id, role],
)
.map_err(|e| e.to_string())?;
drop(conn);
// DB 写入成功后注入 MCP 配置(非致命,注入失败不影响成员关系)
let inject_warning = match mcp_inject::inject_project_mcp(&project_id) {
Ok(()) => None,
@ -220,7 +227,8 @@ pub fn add_group_member(
Ok(inject_warning)
}
#[tauri::command]
// 会经 claude CLI 清理客户端配置:在线程池执行,不占界面线程
#[tauri::command(async)]
pub fn remove_group_member(project_id: String, group_id: String) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 仅当项目退出后不再属于任何组时才清除 MCP 配置(单一 lian-jing key 为全项目共用)
@ -231,6 +239,13 @@ pub fn remove_group_member(project_id: String, group_id: String) -> Result<(), S
|row| row.get(0),
)
.unwrap_or(0);
// 先删成员关系再清 MCP 配置:批量重注入在锁内复查成员关系,先删可避免把它重新注入
conn.execute(
"DELETE FROM project_group_map WHERE project_id = ?1 AND group_id = ?2",
params![project_id, group_id],
)
.map_err(|e| e.to_string())?;
drop(conn);
if in_other_group == 0 {
if let Err(e) = mcp_inject::remove_project_mcp(&project_id) {
let ctx =
@ -243,10 +258,5 @@ pub fn remove_group_member(project_id: String, group_id: String) -> Result<(), S
);
}
}
conn.execute(
"DELETE FROM project_group_map WHERE project_id = ?1 AND group_id = ?2",
params![project_id, group_id],
)
.map_err(|e| e.to_string())?;
Ok(())
}

View File

@ -276,7 +276,12 @@ pub fn update_project(id: String, input: UpdateProjectInput) -> Result<(), Strin
)
.map_err(|e| e.to_string())?;
// 2. 更新 workspace
// 2. 更新 workspace(路径或平台变化时,先记下旧根目录,供客户端侧配置迁移)
let root_may_change =
input.win_path.is_some() || input.wsl_path.is_some() || input.platform.is_some();
let old_root = root_may_change
.then(|| crate::mcp_inject::project_root_with_conn(&conn, &id).ok())
.flatten();
conn.execute(
"UPDATE project_workspaces SET
wsl_path = COALESCE(?1, wsl_path),
@ -297,6 +302,16 @@ pub fn update_project(id: String, input: UpdateProjectInput) -> Result<(), Strin
)
.map_err(|e| e.to_string())?;
// 根目录变化:Claude local 条目按目录挂载、DSH 白名单按根目录匹配,都要迁移到新根。
// 会调用 claude CLI,放到后台线程,避免同步命令卡住界面。
if root_may_change {
let new_root = crate::mcp_inject::project_root_with_conn(&conn, &id).ok();
if crate::mcp_inject::project_root_changed(old_root.as_ref(), new_root.as_ref()) {
drop(conn);
std::thread::spawn(move || crate::mcp_inject::on_project_root_changed(&id, old_root));
}
}
Ok(())
}
@ -312,7 +327,8 @@ pub fn update_project_tags(id: String, tags: String) -> Result<(), String> {
Ok(())
}
#[tauri::command]
// 会经 claude CLI 清理客户端配置:在线程池执行,不占界面线程
#[tauri::command(async)]
pub fn delete_project(id: String, delete_local: Option<bool>) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;

View File

@ -76,7 +76,8 @@ pub fn get_settings(space_id: Option<String>) -> Result<HashMap<String, String>,
}
/// 更新 settings:全局 key 写入 settings 表;其余写入 space_settings 表
#[tauri::command]
// 端口变更会重注入全部项目(含 claude CLI):在线程池执行,不占界面线程
#[tauri::command(async)]
pub fn update_settings(
settings: HashMap<String, String>,
space_id: Option<String>,

View File

@ -119,11 +119,15 @@ pub fn run() {
// ── 三端共用的稳定 helper 入口与 DSH 托管块(best-effort,失败只记日志)──
// 每次启动把 shim 指向当前 exe,换构建目录后各客户端无需逐项目重注入。
if let Err(error) = mcp_inject::ensure_header_shim() {
db::log_event("mcp_inject", "warn", &format!("MCP helper 入口写入失败: {error}"), None);
}
if let Err(error) = mcp_inject::ensure_dsh_patch(mcp_port) {
db::log_event("mcp_inject", "warn", &format!("DSH 托管块写入失败: {error}"), None);
db::log_event(
"mcp_inject",
"warn",
&format!("MCP helper 入口写入失败: {error}"),
Some(r#"{"client":"shim"}"#),
);
}
// 刷新 DSH 白名单要读所有项目根目录的 context.json,放到后台线程,不拖慢窗口启动
std::thread::spawn(|| mcp_inject::refresh_dsh_patch(None));
// ── MCP 单一服务器化一次性迁移(settings 标记位防重复)────
std::thread::spawn(|| {

File diff suppressed because it is too large Load Diff

View File

@ -1603,6 +1603,19 @@ async fn handle_scoped_sse_message(
headers: HeaderMap,
Json(req): Json<RpcRequest>,
) -> Response {
// 握手后项目可能被删除:每条消息复核登记。只移除确实属于该项目的会话,
// 不能凭请求里任意给出的 session_id 删掉别的会话。
if !scoped_project_registered(&project_id) {
let mut guard = sessions.lock().await;
let owned = guard
.get(&query.session_id)
.and_then(|entry| entry.project_scope.as_deref())
== Some(project_id.as_str());
if owned {
guard.remove(&query.session_id);
}
return unknown_project_response(&project_id, req.id.clone());
}
if authorize_sensitive_request(
&req,
Some(&project_id),

View File

@ -1292,6 +1292,8 @@ fn get_diagnostics() -> Result<String, String> {
AND id IN (
SELECT MAX(id) FROM runtime_logs
WHERE category = 'mcp_inject'
AND json_valid(context)
AND json_extract(context, '$.project_id') IS NOT NULL
GROUP BY json_extract(context, '$.project_id')
)
ORDER BY ts DESC