feat(onboard/parity): 客户端适配器注册表与三端结构化注入结果,onboard 模块投影无关适应度测试(P8b、A1a) #33
@ -513,7 +513,7 @@ modules:
|
|||||||
status: in_progress
|
status: in_progress
|
||||||
progress: 43
|
progress: 43
|
||||||
updated: 2026-10-03
|
updated: 2026-10-03
|
||||||
note: "P1/P2/P3/P3b/P7/P9/P10 完成:三端(Claude Code、Codex 含 Desktop、DSH 含桌面 GUI)在 Enterprise 经 %USERPROFILE%\\.lian-jing 稳定入口连项目专用地址,人终验通过。Claude 条目经 claude CLI 写入、以 claude mcp get 为准;DSH 由托管块全局行(headless)+ 会话级插件(GUI/web/acp 按会话工作目录)路由;接入写入幂等;接入包改写块外带未提交改动的托管块文件时给出混合文件预警;P7 实测 MCP 服务器说明仅 Claude/DSH 带入模型上下文、Codex 不带,引导提示保留在 skill。2026-10-03 P8 一键接入设计定稿(docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md):A1 不前置;P4 分 L0 静态 / L1 零模型 / L3 无头会话,P8 只依赖 L1(P4a);拆为 P8-0、P8a–P8g 与 P4-0、P4a–P4e(P8-0、P8a、P4-0 已完成;P8a 把「已接入」统一为有项目 token;P4-0 实测六项假设,Codex 只在目录受信任时加载项目配置、Claude 用无效模型名可零额度取 init 并入 L1),P8 进行中;进度按拆分后卡数重算。待做 P5、P6(基于 P8c 的 WriteSink)。"
|
note: "P1/P2/P3/P3b/P7/P9/P10 完成:三端(Claude Code、Codex 含 Desktop、DSH 含桌面 GUI)在 Enterprise 经 %USERPROFILE%\\.lian-jing 稳定入口连项目专用地址,人终验通过。Claude 条目经 claude CLI 写入、以 claude mcp get 为准;DSH 由托管块全局行(headless)+ 会话级插件(GUI/web/acp 按会话工作目录)路由;接入写入幂等;接入包改写块外带未提交改动的托管块文件时给出混合文件预警;P7 实测 MCP 服务器说明仅 Claude/DSH 带入模型上下文、Codex 不带,引导提示保留在 skill。2026-10-03 P8 一键接入设计定稿(docs/tasks/2026-10-03-p8-onboarding-oneclick-design.md):A1 不前置;P4 分 L0 静态 / L1 零模型 / L3 无头会话,P8 只依赖 L1(P4a);拆为 P8-0、P8a–P8g 与 P4-0、P4a–P4e(P8-0、P8a、P4-0 已完成,P8b 适配器注册表与结构化注入结果已实现、待合并后实测;P8a 把「已接入」统一为有项目 token;P4-0 实测六项假设,Codex 只在目录受信任时加载项目配置、Claude 用无效模型名可零额度取 init 并入 L1),P8 进行中;进度按拆分后卡数重算。待做 P5、P6(基于 P8c 的 WriteSink)。"
|
||||||
position: [3500, 950]
|
position: [3500, 950]
|
||||||
|
|
||||||
- id: architecture-evolution
|
- id: architecture-evolution
|
||||||
|
|||||||
@ -235,13 +235,16 @@ skill 只是五个接入面之一,兼容层覆盖全部五面。
|
|||||||
- 实现(2026-10-03):mcp_inject 新增 is_project_injected(store, project_id)(有项目 MCP token 即已接入),DSH 白名单、on_project_root_changed、list_all_projects 改为调用它;reinject_all_mcp 的目标由「组成员」扩为「组成员 ∪ 有 token 的登记项目」(reinject_targets),锁内复查改为 is_reinject_target(仍是组成员或仍有 token),连接与凭据存储由调用方注入。list_all_projects 拆出 render_all_projects(conn, store),顺带去掉循环内的二次取池。行为变化:独立接入(不在任何组)的项目在端口变更后也会被重注入;list_all_projects 对只剩兼容文件、token 已删的项目改报「未注入」。ensure_standalone_group 的「已在组内不注入」由 P8d 编排本体解决,本卡不动。
|
- 实现(2026-10-03):mcp_inject 新增 is_project_injected(store, project_id)(有项目 MCP token 即已接入),DSH 白名单、on_project_root_changed、list_all_projects 改为调用它;reinject_all_mcp 的目标由「组成员」扩为「组成员 ∪ 有 token 的登记项目」(reinject_targets),锁内复查改为 is_reinject_target(仍是组成员或仍有 token),连接与凭据存储由调用方注入。list_all_projects 拆出 render_all_projects(conn, store),顺带去掉循环内的二次取池。行为变化:独立接入(不在任何组)的项目在端口变更后也会被重注入;list_all_projects 对只剩兼容文件、token 已删的项目改报「未注入」。ensure_standalone_group 的「已在组内不注入」由 P8d 编排本体解决,本卡不动。
|
||||||
- verification: "2026-10-03:cargo test 286 passed / 0 failed / 5 ignored;新增 reinject_targets_follow_single_injected_judgment(组成员无 token 仍是目标、独立有 token 进入目标、删 token 后锁内复查为否且移出目标)与 list_all_projects_judges_injection_by_token_not_compat_file(只有 liangjing.json 无 token → 未注入;有 token 删兼容文件 → 已注入;删 token → 未注入)。变异:屏蔽 reinject_targets 的 token 分支、list_all_projects 改回看 liangjing.json,两条新测试均失败。契约同步 docs/ai-context/project-context.yaml「被管理项目身份上下文」。"
|
- verification: "2026-10-03:cargo test 286 passed / 0 failed / 5 ignored;新增 reinject_targets_follow_single_injected_judgment(组成员无 token 仍是目标、独立有 token 进入目标、删 token 后锁内复查为否且移出目标)与 list_all_projects_judges_injection_by_token_not_compat_file(只有 liangjing.json 无 token → 未注入;有 token 删兼容文件 → 已注入;删 token → 未注入)。变异:屏蔽 reinject_targets 的 token 分支、list_all_projects 改回看 liangjing.json,两条新测试均失败。契约同步 docs/ai-context/project-context.yaml「被管理项目身份上下文」。"
|
||||||
|
|
||||||
### 📋 P8b — 客户端适配器注册表 + 结构化注入结果 + A1a 适应度测试
|
### 🔵 P8b — 客户端适配器注册表 + 结构化注入结果 + A1a 适应度测试
|
||||||
- status: todo
|
- status: in_progress
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- files: src-tauri/src/onboard/mod.rs, src-tauri/src/onboard/clients.rs, src-tauri/src/mcp_inject.rs, src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
- files: src-tauri/src/onboard/mod.rs, src-tauri/src/onboard/clients.rs, src-tauri/src/mcp_inject.rs, src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
||||||
- depends: P8a
|
- depends: P8a
|
||||||
- acceptance: "agent_verify: [cargo test onboard::clients] 注册表恰好列出三端,每端声明 detect、static_status、apply;sync_claude_local 的每条分支映射到一种 ClientOutcome(converged / applied / refreshed / manual_kept / other_project / unavailable{reason} / skipped{control_plane|wsl});DSH 未安装记 not_installed,块外有手写条目记 manual_kept,缺 claude CLI 时 claude 记 unavailable、其余照常;原有 log_event 的类别和文案不变。[cargo test] onboard_module_is_projection_free:src/onboard/ 下源码不含 tauri::、db::pool(、#[tauri::command]。[cargo test onboarding] 新增 clients 字段不改变 checks 计数和 VerifyState,控制面和 WSL 记 not_applicable、不拉低 pass。[MCP] 对 enterprise 连续两次 inject_mcp:第二次三端都是 converged,耗时 <1 秒(未起 claude CLI),四个项目文件 mtime 不变。"
|
- acceptance: "agent_verify: [cargo test onboard::clients] 注册表恰好列出三端,每端声明 detect、static_status、apply;sync_claude_local 的每条分支映射到一种 ClientOutcome(converged / applied / refreshed / manual_kept / other_project / unavailable{reason} / skipped{control_plane|wsl});DSH 未安装记 not_installed,块外有手写条目记 manual_kept,缺 claude CLI 时 claude 记 unavailable、其余照常;原有 log_event 的类别和文案不变。[cargo test] onboard_module_is_projection_free:src/onboard/ 下源码不含 tauri::、db::pool(、#[tauri::command]。[cargo test onboarding] 新增 clients 字段不改变 checks 计数和 VerifyState,控制面和 WSL 记 not_applicable、不拉低 pass。[MCP] 对 enterprise 连续两次 inject_mcp:第二次三端都是 converged,耗时 <1 秒(未起 claude CLI),四个项目文件 mtime 不变。"
|
||||||
- 起因:inject_project_mcp 返回 Result<(),String>,sync_claude_local 返回 (),refresh_dsh_patch 失败只写日志——Claude 与 DSH 的成败没有结构化结果,直接编排会把失败报成成功(R4),所以 P8d 必须排在本卡之后。
|
- 起因:inject_project_mcp 返回 Result<(),String>,sync_claude_local 返回 (),refresh_dsh_patch 失败只写日志——Claude 与 DSH 的成败没有结构化结果,直接编排会把失败报成成功(R4),所以 P8d 必须排在本卡之后。
|
||||||
|
- 实现(2026-10-03,分支 feat/agent-client-parity-p8):新增 src-tauri/src/onboard/(mod.rs 含适应度测试;clients.rs 定义 ClientId、ClientOutcome、ClientReport、ProjectFacts、DesiredMcp、ClientAdapter trait 与 registry())。三端适配器实现在 mcp_inject(需要其私有辅助函数),注册表反向接入。inject_project_mcp_locked 写完项目身份与 Claude 兼容投影后经 registry() 逐端 apply 并收集报告;新增 inject_project_mcp_report 返回逐端结果,inject_project_mcp 签名不变,只有 Codex 托管块写入失败算整体失败(沿用旧语义)。sync_claude_local 抽出依赖注入版 sync_claude_local_with(Claude 配置与 CLI 执行器由调用方给出),每条分支返回一种 ClientOutcome,日志类别与文案不变;ensure_dsh_patch 抽出 ensure_dsh_patch_at(DSH 目录、凭据存储、会话插件引用注入),refresh_dsh_patch 返回结果;upsert_codex_mcp_config 返回 converged/applied/manual_kept。只读判定 claude_static_outcome / codex_static_outcome / dsh_static_outcome + client_static_status 供 verify_onboarding 的 clients 附加字段(MCP 投影)。inject_mcp 回执改为逐端列出实际结果。
|
||||||
|
- 行为变化(有意为之,R5):① 项目 .codex/config.toml 已手写 [mcp_servers.lian-jing] 时由整体报错改为 Codex 记 manual_kept、Claude 与 DSH 照常接入;② Codex 托管块写入失败时 Claude 与 DSH 仍会完成(旧代码先写 Codex、失败即中止),inject_project_mcp 仍返回 Err;③ WSL 项目不再对 Claude local 起 CLI,记 skipped{wsl}(2026-07-04 放弃 WSL 路径项目裁决;DSH 白名单本就排除 WSL);④ Codex 托管块的写入顺序移到 Claude 兼容投影之后。
|
||||||
|
- verification: "2026-10-03:cargo test 295 passed / 0 failed / 5 ignored(新增 9 条:onboard::clients 4 条——注册表恰好三端且顺序固定、settled 判定、扁平序列化、WSL 根识别;onboard 适应度测试;mcp_inject 4 条——claude_sync_maps_every_branch_to_an_outcome 用假 CLI 覆盖 converged×2/unavailable×3/applied/failed×3(含回滚调用序列 get→remove→add-json→add-json)/refreshed/removed/other_project/manual_kept/needs_human,static_outcomes_are_read_only_judgments,dsh_patch_reports_not_installed_applied_converged_and_manual_kept(临时 DSH 目录,手写条目不被覆盖),adapters_skip_control_plane_and_wsl_without_touching_disk;原有 Codex 手写冲突测试改断言 manual_kept)。[MCP] 两次 inject_mcp 的耗时与 mtime 断言须在合并后、运行中的炼境重编译生效时实测,通过后本卡标 ✅。"
|
||||||
|
|
||||||
### 📋 P8c — 写入前计划:WriteSink + plan_onboarding
|
### 📋 P8c — 写入前计划:WriteSink + plan_onboarding
|
||||||
- status: todo
|
- status: todo
|
||||||
|
|||||||
@ -24,8 +24,9 @@ progress: 0
|
|||||||
4. 复用主 exe 做 CLI 还要处理控制台挂接(release 为 windows_subsystem="windows")和 single-instance,那是拆 crate 时才需要解决的设计问题。
|
4. 复用主 exe 做 CLI 还要处理控制台挂接(release 为 windows_subsystem="windows")和 single-instance,那是拆 crate 时才需要解决的设计问题。
|
||||||
结论:P8 需要的是 A1 的原则(本体不依赖投影),不是 crate 拆分。
|
结论:P8 需要的是 A1 的原则(本体不依赖投影),不是 crate 拆分。
|
||||||
|
|
||||||
### 📋 A1a — onboard/ 模块的投影无关约束(随 agent-client-parity P8b 落地)
|
### ✅ A1a — onboard/ 模块的投影无关约束(随 agent-client-parity P8b 落地)
|
||||||
- status: todo
|
- status: done
|
||||||
|
- verification: "2026-10-03(分支 feat/agent-client-parity-p8):src-tauri/src/onboard/mod.rs 内 onboard_module_is_projection_free 逐个读取 onboard/*.rs,出现 tauri::、db::pool(、#[tauri::command] 即失败(模式拼接书写,测试自身不命中);当前 mod.rs 与 clients.rs 通过。ProcessRunner、ClientLocator 尚未引入(P4d 引入时同受此约束);三端适配器实现暂在 mcp_inject(需其私有辅助函数),按绞杀者模式在 P8d 起逐步迁入。"
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- depends: agent-client-parity P8b
|
- depends: agent-client-parity P8b
|
||||||
- acceptance: "agent_verify: [cargo test] onboard_module_is_projection_free——src-tauri/src/onboard/ 下的源码不含 tauri::、db::pool(、#[tauri::command];连接、CredentialStore、ProcessRunner、ClientLocator 都由调用方注入,结果一律是 serde 结构体。"
|
- acceptance: "agent_verify: [cargo test] onboard_module_is_projection_free——src-tauri/src/onboard/ 下的源码不含 tauri::、db::pool(、#[tauri::command];连接、CredentialStore、ProcessRunner、ClientLocator 都由调用方注入,结果一律是 serde 结构体。"
|
||||||
|
|||||||
@ -34,6 +34,7 @@ src-tauri/src/mcp_tools.rs MCP 工具 schema + dispatch + 实现(工具
|
|||||||
src-tauri/src/envoy.rs 使者回传本体(三端回传写入、处置回执、待处置列表;连接由调用方注入)
|
src-tauri/src/envoy.rs 使者回传本体(三端回传写入、处置回执、待处置列表;连接由调用方注入)
|
||||||
src-tauri/src/mcp_webhook.rs Gitea webhook 处理(push/PR 事件写入飞轮)
|
src-tauri/src/mcp_webhook.rs Gitea webhook 处理(push/PR 事件写入飞轮)
|
||||||
src-tauri/src/mcp_inject.rs 接入注入与三端适配(canonical context、Codex 项目配置、Claude local 条目、DSH 托管块、%USERPROFILE%\.lian-jing\bin 稳定 helper 入口)
|
src-tauri/src/mcp_inject.rs 接入注入与三端适配(canonical context、Codex 项目配置、Claude local 条目、DSH 托管块、%USERPROFILE%\.lian-jing\bin 稳定 helper 入口)
|
||||||
|
src-tauri/src/onboard/ 一键接入本体(客户端适配器注册表与结构化结果;不依赖 Tauri、不直接取连接池,适应度测试机器校验)
|
||||||
src-tauri/resources/onboarding/ 接入包分发模板(AGENTS.md/lefthook/git-workflow)
|
src-tauri/resources/onboarding/ 接入包分发模板(AGENTS.md/lefthook/git-workflow)
|
||||||
.blueprint/ 项目蓝图(manifest + 模块详情 + 任务卡)
|
.blueprint/ 项目蓝图(manifest + 模块详情 + 任务卡)
|
||||||
docs/ai-context/ AI 上下文文档(速查卡、契约映射)
|
docs/ai-context/ AI 上下文文档(速查卡、契约映射)
|
||||||
|
|||||||
@ -26,7 +26,7 @@ contracts:
|
|||||||
- ".lian-jing/context.json"
|
- ".lian-jing/context.json"
|
||||||
- ".lian-jing/capability-packs.json"
|
- ".lian-jing/capability-packs.json"
|
||||||
- ".claude/liangjing.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),两份内容必须一致。context.json 可选字段 mcp_scope=global 声明控制面项目(如炼境自身):Claude 不写 local 条目、DSH 不入白名单,保留全局地址与跨项目工具。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。apply_onboarding_pack 的模板结果 TemplateResult 可带 warning(混合文件预警):写入前 AGENTS.md、docs/ai-context/git-workflow.md 或 CLAUDE.md 在 git 中已有托管块(ONBOARDING_MANAGED / BLUEPRINT_MANAGED)之外的未提交改动(未跟踪文件带块外内容也算;比较只忽略 CRLF/LF 与文末托管块连同其前空白——即炼境追加托管块带来的差异,块外空行增删与行尾空格均算改动),且本次确实改写了该文件时给出;只提示不阻断、块外内容不动,非 git 仓库或 git 不可用时静默。CLAUDE.md 只由蓝图规则同步改写,仅在预警时补一条结果。模板分发之后的步骤(lefthook 激活、buff)报错时,已改写的混合文件名并入错误信息返回。「已接入」只有一个判定:本机受保护存储里有项目 MCP token(mcp_inject::is_project_injected);DSH 白名单、根目录迁移、批量重注入(产品组成员 + 有 token 的独立项目)与 list_all_projects 同源,.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),两份内容必须一致。context.json 可选字段 mcp_scope=global 声明控制面项目(如炼境自身):Claude 不写 local 条目、DSH 不入白名单,保留全局地址与跨项目工具。迁移必须保留未知字段;验证必须返回显式状态与 upgrade_hint;升级仅覆盖炼境托管文件,绝不覆盖项目定制或手工 MCP 配置,且不得以客户端全局配置或项目 UI 身份替代项目根目录上下文。apply_onboarding_pack 的模板结果 TemplateResult 可带 warning(混合文件预警):写入前 AGENTS.md、docs/ai-context/git-workflow.md 或 CLAUDE.md 在 git 中已有托管块(ONBOARDING_MANAGED / BLUEPRINT_MANAGED)之外的未提交改动(未跟踪文件带块外内容也算;比较只忽略 CRLF/LF 与文末托管块连同其前空白——即炼境追加托管块带来的差异,块外空行增删与行尾空格均算改动),且本次确实改写了该文件时给出;只提示不阻断、块外内容不动,非 git 仓库或 git 不可用时静默。CLAUDE.md 只由蓝图规则同步改写,仅在预警时补一条结果。模板分发之后的步骤(lefthook 激活、buff)报错时,已改写的混合文件名并入错误信息返回。「已接入」只有一个判定:本机受保护存储里有项目 MCP token(mcp_inject::is_project_injected);DSH 白名单、根目录迁移、批量重注入(产品组成员 + 有 token 的独立项目)与 list_all_projects 同源,.claude/liangjing.json 不作判据。接入经客户端适配器注册表(onboard::clients::registry:Claude、Codex、DSH)逐端收敛并返回结构化结果 ClientOutcome(converged/applied/refreshed/removed/manual_kept/other_project/not_installed/skipped{control_plane|wsl|deferred}/missing/unavailable/needs_human/failed);inject_mcp 回执逐端列出,只有 Codex 托管块写入失败算整体失败;项目 .codex/config.toml 已手写同名 server 记 manual_kept,不再整体报错;verify_onboarding 附加 clients 字段(三端只读状态,不计入 checks、不影响 pass)。src-tauri/src/onboard/ 是一键接入本体模块,源码不得依赖 Tauri、不得直接取全局连接池(适应度测试 onboard_module_is_projection_free)。"
|
||||||
|
|
||||||
- label: Tauri command 接口(前后端契约)
|
- label: Tauri command 接口(前后端契约)
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@ -509,6 +509,9 @@ pub struct OnboardingVerification {
|
|||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub checks: Vec<VerifyCheck>,
|
pub checks: Vec<VerifyCheck>,
|
||||||
pub runtime: crate::commands::settings::AgentRuntimeStatus,
|
pub runtime: crate::commands::settings::AgentRuntimeStatus,
|
||||||
|
/// 三端客户端对本项目的只读配置状态(P8b 附加字段):不计入 checks、不影响 pass。
|
||||||
|
#[serde(default)]
|
||||||
|
pub clients: Vec<crate::onboard::clients::ClientReport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn summarize_onboarding(
|
pub fn summarize_onboarding(
|
||||||
@ -528,6 +531,7 @@ pub fn summarize_onboarding(
|
|||||||
),
|
),
|
||||||
checks,
|
checks,
|
||||||
runtime,
|
runtime,
|
||||||
|
clients: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1065,7 +1069,11 @@ pub fn verify_onboarding(
|
|||||||
let (pass, detail) = check_skill(read(".agents/skills/lian-jing/SKILL.md").as_deref());
|
let (pass, detail) = check_skill(read(".agents/skills/lian-jing/SKILL.md").as_deref());
|
||||||
checks.push(VerifyCheck::from_pass("Codex 炼境技能已分发", pass, detail));
|
checks.push(VerifyCheck::from_pass("Codex 炼境技能已分发", pass, detail));
|
||||||
let (pass, detail) = check_skill(read(".claude/skills/lian-jing/SKILL.md").as_deref());
|
let (pass, detail) = check_skill(read(".claude/skills/lian-jing/SKILL.md").as_deref());
|
||||||
checks.push(VerifyCheck::from_pass("Claude 炼境技能已分发", pass, detail));
|
checks.push(VerifyCheck::from_pass(
|
||||||
|
"Claude 炼境技能已分发",
|
||||||
|
pass,
|
||||||
|
detail,
|
||||||
|
));
|
||||||
checks.push(check_codex_mcp(
|
checks.push(check_codex_mcp(
|
||||||
read(".codex/config.toml").as_deref(),
|
read(".codex/config.toml").as_deref(),
|
||||||
project_id,
|
project_id,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ mod mcp_server;
|
|||||||
mod mcp_tools;
|
mod mcp_tools;
|
||||||
mod mcp_webhook;
|
mod mcp_webhook;
|
||||||
mod models;
|
mod models;
|
||||||
|
mod onboard;
|
||||||
use git2;
|
use git2;
|
||||||
|
|
||||||
/// Codex 的 http_headers_helper 入口;只被本机同一可执行文件的专用参数调用。
|
/// Codex 的 http_headers_helper 入口;只被本机同一可执行文件的专用参数调用。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -627,6 +627,40 @@ fn apply_tool_annotations(result: &mut Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// inject_mcp 的回执按三端实际结果逐行给出(P8b):不再笼统宣称「已配置」。
|
||||||
|
/// Codex 托管块写入失败沿用旧语义整体报错;Claude 与 DSH 的未就绪状态如实列出但不报错。
|
||||||
|
fn render_inject_report(
|
||||||
|
project_id: &str,
|
||||||
|
reports: &[crate::onboard::clients::ClientReport],
|
||||||
|
) -> Result<String, String> {
|
||||||
|
use crate::onboard::clients::{ClientId, ClientOutcome};
|
||||||
|
let lines: Vec<String> = reports
|
||||||
|
.iter()
|
||||||
|
.map(|report| {
|
||||||
|
let name = match report.client {
|
||||||
|
ClientId::Claude => "Claude Code",
|
||||||
|
ClientId::Codex => "Codex",
|
||||||
|
ClientId::Dsh => "DSH",
|
||||||
|
};
|
||||||
|
format!("- {name}:{}", report.outcome.describe())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let all_settled = reports.iter().all(|report| report.outcome.is_settled());
|
||||||
|
let text = format!(
|
||||||
|
"MCP 注入{}:project_id={project_id}(单一 lian-jing 服务器,项目专用地址)\n{}\nCodex 只在该目录受信任后才加载项目配置;已打开的会话需重开才生效。",
|
||||||
|
if all_settled { "完成" } else { "部分完成" },
|
||||||
|
lines.join("\n")
|
||||||
|
);
|
||||||
|
let codex_failed = reports.iter().any(|report| {
|
||||||
|
report.client == ClientId::Codex && matches!(report.outcome, ClientOutcome::Failed { .. })
|
||||||
|
});
|
||||||
|
if codex_failed {
|
||||||
|
Err(text)
|
||||||
|
} else {
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 只做 JSON 类型解析;取值语义(正整数、分支名格式)由能力本体 `DeliveryQuery::new` 单点校验。
|
/// 只做 JSON 类型解析;取值语义(正整数、分支名格式)由能力本体 `DeliveryQuery::new` 单点校验。
|
||||||
/// 缺省或 null 视为未指定;显式给出但类型不对时报错,不静默回退到主工作区当前分支。
|
/// 缺省或 null 视为未指定;显式给出但类型不对时报错,不静默回退到主工作区当前分支。
|
||||||
fn parse_delivery_query(
|
fn parse_delivery_query(
|
||||||
@ -936,8 +970,8 @@ async fn tools_call_with_scope(params: Option<&Value>, project_scope: Option<Str
|
|||||||
}
|
}
|
||||||
"inject_mcp" => {
|
"inject_mcp" => {
|
||||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
crate::mcp_inject::inject_project_mcp(pid)
|
crate::mcp_inject::inject_project_mcp_report(pid)
|
||||||
.map(|_| format!("MCP 注入成功:project_id={}(已配置 Codex 与 Claude;单一 lian-jing 服务器,无需 group_id)", pid))
|
.and_then(|reports| render_inject_report(pid, &reports))
|
||||||
}
|
}
|
||||||
"get_ci_status" => {
|
"get_ci_status" => {
|
||||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
@ -1376,8 +1410,9 @@ async fn tools_call_with_scope(params: Option<&Value>, project_scope: Option<Str
|
|||||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||||
let checks = crate::commands::onboarding::verify_onboarding(&conn, pid, &root);
|
let checks = crate::commands::onboarding::verify_onboarding(&conn, pid, &root);
|
||||||
let runtime = crate::commands::settings::server_observed_runtime_status(pid, &root);
|
let runtime = crate::commands::settings::server_observed_runtime_status(pid, &root);
|
||||||
let report =
|
let mut report =
|
||||||
crate::commands::onboarding::summarize_onboarding(pid, checks, runtime);
|
crate::commands::onboarding::summarize_onboarding(pid, checks, runtime);
|
||||||
|
report.clients = crate::mcp_inject::client_static_status(pid, &root);
|
||||||
serde_json::to_string_pretty(&report)
|
serde_json::to_string_pretty(&report)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
213
src-tauri/src/onboard/clients.rs
Normal file
213
src-tauri/src/onboard/clients.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
//! 客户端适配器注册表与结构化结果(agent-client-parity P8b)。
|
||||||
|
//!
|
||||||
|
//! 每个 agent 客户端(Claude Code、Codex、DSH)声明三件事:本机是否装有(detect)、本项目当前状态的
|
||||||
|
//! 只读判定(static_status)、收敛到期望状态(apply)。接入编排与 verify 只经注册表遍历,
|
||||||
|
//! 不按客户端写分支;新增客户端 = 新增一个适配器并登记到 `registry()`。
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ClientId {
|
||||||
|
Claude,
|
||||||
|
Codex,
|
||||||
|
Dsh,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SkipReason {
|
||||||
|
/// 控制面项目(mcp_scope=global):不写项目级条目,回退全局地址
|
||||||
|
ControlPlane,
|
||||||
|
/// WSL 路径项目(2026-07-04 裁决放弃):Claude local 与 DSH 不支持
|
||||||
|
Wsl,
|
||||||
|
/// 批量重注入:逐项目跳过,由调用方在末尾统一刷新一次
|
||||||
|
Deferred,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单个客户端在一次接入或静态检查中的结果。
|
||||||
|
/// - 成功态:converged(已一致、未写)、applied(新写入)、refreshed(替换了旧托管条目)、removed(按期望移除);
|
||||||
|
/// - 按边界不改动:manual_kept(手写同名配置)、other_project(共享项目键属于他项目);
|
||||||
|
/// - 不计失败:not_installed、skipped;
|
||||||
|
/// - 未就绪:missing(静态检查:已安装但本项目未配置或已过期)、unavailable(无法确认现状)、
|
||||||
|
/// needs_human(需人工清理)、failed(尝试写入但失败)。
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "state", rename_all = "snake_case")]
|
||||||
|
pub enum ClientOutcome {
|
||||||
|
Converged,
|
||||||
|
Applied,
|
||||||
|
Refreshed,
|
||||||
|
Removed,
|
||||||
|
ManualKept,
|
||||||
|
OtherProject { owner: String },
|
||||||
|
NotInstalled,
|
||||||
|
Skipped { reason: SkipReason },
|
||||||
|
Missing,
|
||||||
|
Unavailable { reason: String },
|
||||||
|
NeedsHuman { reason: String },
|
||||||
|
Failed { reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientOutcome {
|
||||||
|
/// 该端对本项目已处于期望状态(含按边界保留手写配置、未安装、不适用)——不计为失败。
|
||||||
|
pub fn is_settled(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Converged
|
||||||
|
| Self::Applied
|
||||||
|
| Self::Refreshed
|
||||||
|
| Self::Removed
|
||||||
|
| Self::ManualKept
|
||||||
|
| Self::NotInstalled
|
||||||
|
| Self::Skipped { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 给人和 agent 读的一行中文说明。
|
||||||
|
pub fn describe(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Converged => "已一致(未改动)".into(),
|
||||||
|
Self::Applied => "已写入".into(),
|
||||||
|
Self::Refreshed => "已刷新".into(),
|
||||||
|
Self::Removed => "已移除".into(),
|
||||||
|
Self::ManualKept => "保留手写配置,炼境未覆盖".into(),
|
||||||
|
Self::OtherProject { owner } => {
|
||||||
|
format!("生效条目属于项目 {owner}(共享项目键),炼境未改动")
|
||||||
|
}
|
||||||
|
Self::NotInstalled => "未安装".into(),
|
||||||
|
Self::Skipped { reason } => match reason {
|
||||||
|
SkipReason::ControlPlane => "不适用(控制面项目走全局地址)".into(),
|
||||||
|
SkipReason::Wsl => "不支持(WSL 路径项目)".into(),
|
||||||
|
SkipReason::Deferred => "本次跳过(批量操作末尾统一刷新)".into(),
|
||||||
|
},
|
||||||
|
Self::Missing => "未配置或已过期".into(),
|
||||||
|
Self::Unavailable { reason } => format!("无法确认:{reason}"),
|
||||||
|
Self::NeedsHuman { reason } => format!("需人工处理:{reason}"),
|
||||||
|
Self::Failed { reason } => format!("失败:{reason}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ClientReport {
|
||||||
|
pub client: ClientId,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub outcome: ClientOutcome,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientReport {
|
||||||
|
pub fn new(client: ClientId, outcome: ClientOutcome) -> Self {
|
||||||
|
Self { client, outcome }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 项目在本机的事实,由调用方从登记与磁盘读取后注入。
|
||||||
|
pub struct ProjectFacts<'a> {
|
||||||
|
pub project_id: &'a str,
|
||||||
|
pub root: &'a Path,
|
||||||
|
/// 根目录 context.json 声明 mcp_scope=global
|
||||||
|
pub control_plane: bool,
|
||||||
|
/// 根目录是 WSL UNC 路径
|
||||||
|
pub wsl: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本次接入的期望连接参数(项目专用地址 + 稳定 helper 命令)。
|
||||||
|
pub struct DesiredMcp {
|
||||||
|
pub port: u16,
|
||||||
|
pub server_url: String,
|
||||||
|
pub header_helper: String,
|
||||||
|
/// false 时 DSH 不逐项目刷新(批量重注入末尾统一刷新)
|
||||||
|
pub refresh_dsh: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ClientAdapter: Sync {
|
||||||
|
fn id(&self) -> ClientId;
|
||||||
|
/// 本机是否装有该客户端(只读、毫秒级,不起子进程)。
|
||||||
|
fn detect(&self) -> bool;
|
||||||
|
/// 只读判定本项目在该端的配置状态,不写任何文件、不起子进程。
|
||||||
|
fn static_status(&self, project: &ProjectFacts, desired: &DesiredMcp) -> ClientOutcome;
|
||||||
|
/// 收敛到期望状态;调用方须持有接入锁。失败返回 failed / unavailable,不 panic、不中断其他端。
|
||||||
|
fn apply(&self, project: &ProjectFacts, desired: &DesiredMcp) -> ClientOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 三端适配器注册表;顺序即报告顺序。
|
||||||
|
pub fn registry() -> [&'static dyn ClientAdapter; 3] {
|
||||||
|
[
|
||||||
|
&crate::mcp_inject::CLAUDE_ADAPTER,
|
||||||
|
&crate::mcp_inject::CODEX_ADAPTER,
|
||||||
|
&crate::mcp_inject::DSH_ADAPTER,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根目录是否为 WSL UNC 路径(\\wsl.localhost\… 或 \\wsl$\…)。
|
||||||
|
pub fn is_wsl_root(root: &Path) -> bool {
|
||||||
|
let text = root.display().to_string().to_ascii_lowercase();
|
||||||
|
text.starts_with(r"\\wsl.localhost\") || text.starts_with(r"\\wsl$\")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_lists_exactly_three_clients_in_order() {
|
||||||
|
let ids: Vec<ClientId> = registry().iter().map(|adapter| adapter.id()).collect();
|
||||||
|
assert_eq!(ids, vec![ClientId::Claude, ClientId::Codex, ClientId::Dsh]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settled_states_exclude_failures_and_missing() {
|
||||||
|
let settled = [
|
||||||
|
ClientOutcome::Converged,
|
||||||
|
ClientOutcome::Applied,
|
||||||
|
ClientOutcome::Refreshed,
|
||||||
|
ClientOutcome::Removed,
|
||||||
|
ClientOutcome::ManualKept,
|
||||||
|
ClientOutcome::NotInstalled,
|
||||||
|
ClientOutcome::Skipped {
|
||||||
|
reason: SkipReason::ControlPlane,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert!(settled.iter().all(ClientOutcome::is_settled));
|
||||||
|
let unsettled = [
|
||||||
|
ClientOutcome::Missing,
|
||||||
|
ClientOutcome::OtherProject { owner: "x".into() },
|
||||||
|
ClientOutcome::Unavailable { reason: "r".into() },
|
||||||
|
ClientOutcome::NeedsHuman { reason: "r".into() },
|
||||||
|
ClientOutcome::Failed { reason: "r".into() },
|
||||||
|
];
|
||||||
|
assert!(!unsettled.iter().any(ClientOutcome::is_settled));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn report_serializes_flat_with_snake_case_state() {
|
||||||
|
let report = ClientReport::new(
|
||||||
|
ClientId::Dsh,
|
||||||
|
ClientOutcome::Skipped {
|
||||||
|
reason: SkipReason::ControlPlane,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&report).unwrap(),
|
||||||
|
serde_json::json!({"client": "dsh", "state": "skipped", "reason": "control_plane"})
|
||||||
|
);
|
||||||
|
let failed = ClientReport::new(
|
||||||
|
ClientId::Claude,
|
||||||
|
ClientOutcome::Unavailable {
|
||||||
|
reason: "claude CLI 未找到".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&failed).unwrap()["state"],
|
||||||
|
"unavailable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wsl_roots_detected_case_insensitively() {
|
||||||
|
assert!(is_wsl_root(Path::new(r"\\wsl.localhost\Ubuntu\home\a")));
|
||||||
|
assert!(is_wsl_root(Path::new(r"\\WSL$\Ubuntu\home\a")));
|
||||||
|
assert!(!is_wsl_root(Path::new(r"E:\code-project\a")));
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src-tauri/src/onboard/mod.rs
Normal file
42
src-tauri/src/onboard/mod.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//! 一键接入(agent-client-parity P8)的本体模块。
|
||||||
|
//!
|
||||||
|
//! 约束(architecture-evolution A1a,由下方 `onboard_module_is_projection_free` 机器校验):
|
||||||
|
//! 本模块源码不依赖 Tauri、不直接取全局连接池、不声明 Tauri command;连接、凭据存储、
|
||||||
|
//! 进程执行由调用方注入,结果一律是 serde 结构体。Tauri command 与 MCP 工具只做薄投影。
|
||||||
|
//! 存量入口(mcp_inject 等)按绞杀者模式逐步改成薄包装,不往这里扩散旧写法。
|
||||||
|
|
||||||
|
pub mod clients;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod fitness_tests {
|
||||||
|
/// 架构适应度测试:onboard/ 下任何源码出现投影层依赖即失败(模式拼接书写,避免本测试自身命中)。
|
||||||
|
#[test]
|
||||||
|
fn onboard_module_is_projection_free() {
|
||||||
|
let forbidden = [
|
||||||
|
["tauri", "::"].concat(),
|
||||||
|
["db::", "pool("].concat(),
|
||||||
|
["#[tauri", "::command"].concat(),
|
||||||
|
];
|
||||||
|
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/onboard");
|
||||||
|
let mut checked = 0;
|
||||||
|
for entry in std::fs::read_dir(&dir).expect("读取 src/onboard") {
|
||||||
|
let path = entry.expect("目录项").path();
|
||||||
|
if path.extension().and_then(|ext| ext.to_str()) != Some("rs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source = std::fs::read_to_string(&path).expect("读取源码");
|
||||||
|
for pattern in &forbidden {
|
||||||
|
assert!(
|
||||||
|
!source.contains(pattern.as_str()),
|
||||||
|
"{} 含投影层依赖 {pattern}:onboard 本体须由调用方注入连接与执行器",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
checked += 1;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
checked >= 2,
|
||||||
|
"至少应检查 mod.rs 与 clients.rs,实际 {checked}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user