fix(onboarding): 接入写入幂等——内容不变不重写,Codex 托管块不再累积空行 #25

Merged
lanrtop merged 2 commits from fix/onboarding-idempotent-writes into master 2026-10-02 12:06:29 +08:00
Owner

⚠️ 本 PR 叠在 #24(feat/agent-client-parity)之上:请先合并 #24,之后本 PR 只剩下面两个提交。

目标

接入写入幂等(蓝图 agent-client-parity P3b):炼境向子项目写文件时,内容不变就不重写;Codex 托管块不再每次注入多一个空行。

为什么

enterprise 未提交改动归属梳理时发现:

  • Codex 托管块每注入一次多一个空行,enterprise 的 .codex/config.toml 已累积 16 行;
  • 子项目开了 core.autocrlf,炼境在内容不变时也用 LF 重写 CRLF 检出的文件,git status 因此误报改动(CONVENTIONS.md、context.json),文件修改时间也被刷新,干扰了「这次改动是谁做的」的判断。

改动

提交 内容
d098905 lib.rs 新增 write_if_content_changed(忽略行尾比较,内容不变不写);sync_blueprint_rules(CONVENTIONS + AGENTS/CLAUDE 托管块)、context.json、.claude/settings.json、Codex 托管块改用它;托管块未变时返回 unchanged;Codex 托管块吸收 END 之后的空行
019d81c 对抗审查修复:JSON 文件按语义比较(客户端改写后键序/末尾换行不同也不重排);CI workflow 的 plan 忽略行尾判 noop,apply 与 generate_workflow 只在变化时落盘;agent_infra 生成文件与 notes-archive 回填统一改用同一函数

自验

  • cargo test --locked:245 passed / 5 ignored。新增回归:Codex 托管块连续注入字节不变并吸收累积空行;CRLF 检出的同内容托管块返回 unchanged 且不被重写;语义相同的 settings.json 保留客户端格式;CRLF 检出的同内容 CI workflow 的 plan 为 noop。
  • 实测 enterprise:注入后 .codex/config.toml 末尾空行 16→0;再次注入,.codex/config.toml、.claude/settings.json、.lian-jing/context.json、.claude/liangjing.json 四个文件修改时间均不变。
  • 对抗审查:9 个 agent,2 条成立已修,5 条驳回(均为本提交之前就存在、且已在本 PR 一并处理或不构成缺陷)。

没验什么

  • sync_blueprint_rules、CI workflow apply / generate_workflow、agent_infra 生成、notes-archive 回填这几条路径只有单元测试,没有在真实子项目上实际触发。
  • 子项目里已经存在的伪改动(enterprise 的 context.json、CONVENTIONS.md)不会因本 PR 消失,需要在子项目里由人刷新一次索引(对这两个文件执行 git add,暂存的是空 diff)。
  • 内容真的变化时不保留原文件行尾(与修改前行为相同),未做处理。

后续(已立卡,未在本 PR 实现)

  • P10:接入包升级前检测目标文件的块外未提交改动,避免制造「炼境 + 项目」混合文件。

🤖 Generated with Claude Code

> ⚠️ 本 PR 叠在 #24(feat/agent-client-parity)之上:请**先合并 #24**,之后本 PR 只剩下面两个提交。 ## 目标 接入写入幂等(蓝图 agent-client-parity P3b):炼境向子项目写文件时,内容不变就不重写;Codex 托管块不再每次注入多一个空行。 ## 为什么 enterprise 未提交改动归属梳理时发现: - Codex 托管块每注入一次多一个空行,enterprise 的 `.codex/config.toml` 已累积 16 行; - 子项目开了 `core.autocrlf`,炼境在内容不变时也用 LF 重写 CRLF 检出的文件,git status 因此误报改动(CONVENTIONS.md、context.json),文件修改时间也被刷新,干扰了「这次改动是谁做的」的判断。 ## 改动 | 提交 | 内容 | |---|---| | d098905 | `lib.rs` 新增 `write_if_content_changed`(忽略行尾比较,内容不变不写);`sync_blueprint_rules`(CONVENTIONS + AGENTS/CLAUDE 托管块)、context.json、`.claude/settings.json`、Codex 托管块改用它;托管块未变时返回 `unchanged`;Codex 托管块吸收 END 之后的空行 | | 019d81c | 对抗审查修复:JSON 文件按语义比较(客户端改写后键序/末尾换行不同也不重排);CI workflow 的 plan 忽略行尾判 noop,apply 与 generate_workflow 只在变化时落盘;agent_infra 生成文件与 notes-archive 回填统一改用同一函数 | ## 自验 - `cargo test --locked`:245 passed / 5 ignored。新增回归:Codex 托管块连续注入字节不变并吸收累积空行;CRLF 检出的同内容托管块返回 unchanged 且不被重写;语义相同的 settings.json 保留客户端格式;CRLF 检出的同内容 CI workflow 的 plan 为 noop。 - 实测 enterprise:注入后 `.codex/config.toml` 末尾空行 16→0;再次注入,`.codex/config.toml`、`.claude/settings.json`、`.lian-jing/context.json`、`.claude/liangjing.json` 四个文件修改时间均不变。 - 对抗审查:9 个 agent,2 条成立已修,5 条驳回(均为本提交之前就存在、且已在本 PR 一并处理或不构成缺陷)。 ## 没验什么 - `sync_blueprint_rules`、CI workflow apply / generate_workflow、agent_infra 生成、notes-archive 回填这几条路径只有单元测试,没有在真实子项目上实际触发。 - 子项目里**已经存在**的伪改动(enterprise 的 context.json、CONVENTIONS.md)不会因本 PR 消失,需要在子项目里由人刷新一次索引(对这两个文件执行 `git add`,暂存的是空 diff)。 - 内容真的变化时不保留原文件行尾(与修改前行为相同),未做处理。 ## 后续(已立卡,未在本 PR 实现) - P10:接入包升级前检测目标文件的块外未提交改动,避免制造「炼境 + 项目」混合文件。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
lanrtop added 17 commits 2026-10-02 10:53:11 +08:00
- P3:炼境启动写 %LOCALAPPDATA%\lian-jing\bin\lianjing-mcp-header.cmd 指向当前 exe;
  Codex 项目配置改为引用该入口,不再写本机 exe 绝对路径;verify_onboarding 对旧写法报可升级
- P1:inject_mcp 以 local scope 为 Claude 注册项目专用地址 + headersHelper(不进仓库);
  DSH 机器级补丁写托管块,!!js 按启动目录选项目地址并经 cmd 调用同一入口,项目外回退全局
- 服务端:scoped route 拒绝未登记的 project_id(404,数据库不可用同样拒绝)

Feature-Confirmed: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 控制面项目(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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
最终审查指出直接编辑 ~/.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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
收尾对抗审查指出:只读扫描 ~/.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>
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
1888f71626
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
人终验发现 Codex Desktop 会话里 lian-jing 完全未加载,Codex 日志为 headers helper exit 1。
根因:Codex Desktop 是 MSIX 打包应用,看不见它安装之后在 %LOCALAPPDATA% / %APPDATA% 下新建的
顶层目录(Invoke-CommandInDesktopPackage 在包上下文对照实测:lian-jing 与新建对照目录均不可见,
与 ACL 无关;已有目录内的新文件、%USERPROFILE% 下的新目录可见)。原验收用包外的 codex exec,未覆盖。

- shim 迁至 %USERPROFILE%\.lian-jing\bin\;Codex 配置、Claude local 条目、DSH 托管块统一引用新入口
- 炼境 exe 位于 AppData 下(安装版默认位置)时,按大小+修改时间指纹复制到入口目录,清理旧副本;
  复制失败退回直接调用当前 exe 并记 warn
- 旧 %LOCALAPPDATA% 入口迁移期同步写入;verify_onboarding 把旧写法报 Legacy 引导 inject_mcp 升级
  (测试断言随之由 Ready 改为 Legacy,理由同上)
- 新增 helper 副本与 AppData 判定测试

复验:Codex 包内(等价 Desktop)、Codex 包外、Claude、DSH 无头四路零凭据探针均返回「开发需求未登记」
(修复前 Codex 包内为「工具不可用」);cargo test 245 passed / 5 ignored。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(mcp/parity): 审查修复——shim 单行退出、副本刷盘后改名、DSH 仅 headless 按目录路由
All checks were successful
Push & PR Check / check (push) Successful in 1m28s
Push & PR Check / check (pull_request) Successful in 1m5s
cca3bf9db1
对抗审查(14 个 agent,3 条核实成立)与 DSH 桌面版只读调研的修复:

- shim 改为调用与退出同一行(`& call exit /b %%ERRORLEVEL%%`):cmd 在命令返回后按字节偏移重读
  批处理,入口恰在 helper 运行期间被改写时,分行写法会执行到错位片段(实测 exit 9009)
- helper exe 副本 sync_all 后再改名:避免断电留下残缺副本却因同名被一直复用
- 旧副本改为入口写好之后再清理;旧位置入口写失败时暂不清理,避免入口悬空
- DSH 托管块只在 headless profile 按启动目录路由:web/acp/tui 一个进程可服务多个工作区,
  按启动目录会让别的工作区会话连到该项目并带上认证(跨项目越权);profile 取 profileContext.name,
  DSH 0.1.x 无该服务时退回看启动参数
- AGENTS.md 入口路径补全反斜杠;蓝图立 P9(DSH 桌面 GUI 会话级插件,待人确认)

验证:cargo test 245 passed;shim 认证成功 exit 0 / 92 字节、错误 digest exit 1;DSH url 表达式按
Loader 方式求值 7 种情形均符合预期;四路零凭据探针(Codex 包内、Codex 包外、Claude、DSH 无头)复跑通过。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(mcp/parity): 更正 Codex Desktop 取认证失败的根因,撤销基于错误前提的 exe 副本逻辑
All checks were successful
Push & PR Check / check (push) Successful in 1m37s
Push & PR Check / check (pull_request) Successful in 1m2s
c4241f075d
2b0a55a 判断「Codex Desktop(MSIX)看不见其安装后在 AppData 下新建的顶层目录」是错的:当时的对照
目录本身也是在 Claude 桌面版的包环境里建的。真正原因:worktree 版炼境由 Claude 桌面版(MSIX)里的
agent 启动,继承了其包环境,写到 %LOCALAPPDATA%\lian-jing 的入口被重定向进
%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Local\,只有 Claude 的进程树看得到。
对照:经 WMI 启动的非包进程在 AppData 下新建的顶层目录,Codex 包内可见;%USERPROFILE% 根不做重定向。

- 入口迁至 %USERPROFILE%\.lian-jing\bin 仍是正解(与炼境在什么环境里启动无关),保留
- 撤销「exe 在 AppData 下就复制副本」逻辑及其测试:exe 总在真实路径上,前提不成立(按「退出」原则)
- 更正代码注释、verify_onboarding 的 Legacy 提示、契约文档与蓝图 P3 返工记录

验证:cargo test 243 passed / 5 ignored(删去 2 个只测副本逻辑的测试)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
enterprise 未提交改动归属梳理发现两处炼境侧缺陷:
- Codex 托管块自带结尾换行,又原样保留 END 之后的换行,每次注入多一个空行(enterprise 已累积 16 行)
- sync_blueprint_rules 与注入在内容不变时也用 LF 重写文件:子项目开 core.autocrlf,CRLF 检出的
  同内容文件被 git status 误报为改动,修改时间也被刷新,干扰改动归属判断

修复:
- lib.rs 新增 write_if_content_changed(忽略行尾比较,内容不变不写)
- sync_blueprint_rules 的 CONVENTIONS 与 AGENTS/CLAUDE 托管块、context.json、.claude/settings.json、
  Codex 托管块统一改用它;托管块未变时返回 unchanged
- Codex 托管块写入吸收 END 之后的空行,块外后续内容以一个空行分隔
- 蓝图记 P3b(本修复)、P10(接入包升级前检测块外未提交改动,待做),P9 补可行性实测记录

验证:cargo test 246 passed;实测 enterprise 注入后 .codex/config.toml 末尾空行 16→0,
再次注入四个托管文件修改时间均不变。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(onboarding): 审查修复——JSON 按语义比较,CI workflow 与生成文件同内容不重写
All checks were successful
Push & PR Check / check (pull_request) Successful in 1m3s
019d81c60e
对抗审查(9 个 agent,2 条成立):
- settings.json / context.json 改为按 JSON 语义比较:Claude Code 改写后键序与末尾换行不同,
  按文本比较仍会让每次注入整文件重排
- plan_cicd_workflow 忽略行尾判 noop:原按字节比较,autocrlf 检出的同内容 workflow 会被判 update,
  并请用户确认一次无变化的写入;apply 与 generate_workflow 只在内容变化时落盘
- 同类遗漏统一改用 write_if_content_changed:agent_infra 生成文件(未变时报 skipped)、notes-archive 回填
- lib.rs 抽出 same_text_ignoring_eol,与 write_if_content_changed 共用同一比较规则

验证:cargo test 245 passed / 5 ignored(新增 JSON 语义与 CI CRLF 回归断言)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lanrtop merged commit 239d9fdd83 into master 2026-10-02 12:06:29 +08:00
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: lanrtop/dev-manager-tauri#25
No description provided.