From c598c9b05be435499efbe44b4ca06a78a49ac21b Mon Sep 17 00:00:00 2001 From: lanrtop Date: Sat, 4 Apr 2026 10:47:27 +0900 Subject: [PATCH] feat: add BlueprintPromptModal for blueprint management and sync functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented BlueprintPromptModal component to handle blueprint prompt generation and rule synchronization. - Added BlueprintStatusBadge to display the current status of blueprints in ProjectCard. - Integrated blueprint status fetching and synchronization in ProjectCard. - Enhanced SettingsPage with a new section for MCP service configuration and batch sync for blueprint rules. - Updated commands to include new blueprint-related functionalities and types. - Changed application name from "Dev Manager" to "炼境" in various components. --- .blueprint/CONVENTIONS.md | 179 ++- .blueprint/manifest.yaml | 92 +- .blueprint/modules/auto-update.md | 6 +- .blueprint/modules/blueprint-feedback.md | 67 +- .blueprint/modules/blueprint-flywheel.md | 55 + .blueprint/modules/blueprint-governance.md | 45 +- .blueprint/modules/blueprint-onboarding.md | 22 +- .blueprint/modules/blueprint-view.md | 29 +- .blueprint/modules/mcp-config-inject.md | 40 + .blueprint/modules/mcp-server.md | 41 + .blueprint/modules/product-group-mcp.md | 25 + .blueprint/modules/tray-service.md | 28 + .blueprint/usage.json | 26 + .github/workflows/release.yml | 4 +- CLAUDE.md | 27 +- src-tauri/Cargo.lock | 99 ++ src-tauri/Cargo.toml | 3 + src-tauri/src/commands/blueprint.rs | 1174 ++++++++++++++++- src-tauri/src/commands/groups.rs | 22 + src-tauri/src/commands/projects.rs | 15 + src-tauri/src/commands/settings.rs | 14 +- src-tauri/src/db.rs | 1 + src-tauri/src/lib.rs | 77 +- src-tauri/src/mcp_inject.rs | 142 ++ src-tauri/src/mcp_server.rs | 289 ++++ src-tauri/tauri.conf.json | 2 +- src/App.tsx | 14 + src/components/dashboard/BlueprintModal.tsx | 678 +++++++++- .../dashboard/BlueprintPromptModal.tsx | 229 ++++ src/components/dashboard/ProjectCard.tsx | 75 +- src/components/layout/Sidebar.tsx | 2 +- src/components/manage/ManagePage.tsx | 28 + src/components/settings/SettingsPage.tsx | 172 ++- src/lib/commands.ts | 89 ++ 34 files changed, 3669 insertions(+), 142 deletions(-) create mode 100644 .blueprint/modules/blueprint-flywheel.md create mode 100644 .blueprint/modules/mcp-config-inject.md create mode 100644 .blueprint/modules/mcp-server.md create mode 100644 .blueprint/modules/product-group-mcp.md create mode 100644 .blueprint/modules/tray-service.md create mode 100644 .blueprint/usage.json create mode 100644 src-tauri/src/mcp_inject.rs create mode 100644 src-tauri/src/mcp_server.rs create mode 100644 src/components/dashboard/BlueprintPromptModal.tsx diff --git a/.blueprint/CONVENTIONS.md b/.blueprint/CONVENTIONS.md index 3c3fa0d..87b2ca4 100644 --- a/.blueprint/CONVENTIONS.md +++ b/.blueprint/CONVENTIONS.md @@ -1,5 +1,5 @@ --- -version: 1.0.0 +version: 1.3.0 updated: 2026-04-03 source: dev-manager-tauri --- @@ -25,10 +25,12 @@ source: dev-manager-tauri | 事件 | 操作 | |------|------| | 用户确认了新需求 | 在 manifest.yaml 新增模块,创建 modules/.md | -| 完成了功能实现 | 更新模块 status → done,标记任务卡 ✅ | +| **开始执行任务卡(信息1)** | **任务卡前缀改为 🔵,status → in_progress(第一行代码之前)** | +| **完成了功能实现(信息2)** | **任务卡前缀改为 ✅,status → done(验收通过之后)** | | 拆分了大需求为子任务 | 在 modules/.md 中添加任务卡 | | 调整了架构方向 | 更新 edges 关系,可能调整 area | | 放弃了某个方向 | 删除对应模块和文件,或标记 status: abandoned | +| **重构已完成的模块(迭代)** | **manifest.iteration 递增,旧任务卡保留,新建替代任务卡,模块加 `## 迭代记录`** | ## 模块状态定义 @@ -52,6 +54,8 @@ modules/.md 中的每个二级标题(`###`)是一张任务卡: - depends: other-module-id - acceptance: 简要描述完成标准 - blocked_reason: (仅 status: blocked 时填写)执行受阻的具体原因 +- iteration: 1 # 可选,省略时继承 manifest 顶层 iteration 值 +- replaces: 旧任务标题 # 可选,仅重构任务填写,说明本卡替代哪张旧卡 详细说明(可选)。 ``` @@ -60,8 +64,8 @@ modules/.md 中的每个二级标题(`###`)是一张任务卡: | 前缀 | status | 含义 | |------|--------|------| -| ✅ | done | 已完成 | -| 🔵 | in_progress | 进行中 | +| ✅ | done | 已完成(信息1 + 信息2 均已写入) | +| 🔵 | in_progress | 执行中或被中断(只有信息1,等待信息2) | | 📋 | todo | 可派发(信息充足) | | 💭 | concept | 构思中 | | 🔴 | blocked | 执行受阻 | @@ -78,6 +82,15 @@ modules/.md 中的每个二级标题(`###`)是一张任务卡: ## manifest.yaml 规范 +### 顶层字段 + +```yaml +version: 1 +name: 项目名称 +iteration: 1 # 当前迭代号(默认 1,重构已完成模块时手动递增) +updated: YYYY-MM-DD +``` + ### 模块字段 ```yaml @@ -109,7 +122,64 @@ modules/.md 中的每个二级标题(`###`)是一张任务卡: 模块的 progress 由其任务卡的完成比例推算: - 无任务卡的模块:手动设置 progress -- 有任务卡的模块:`完成数 / 总数 × 100`(取整) +- 有任务卡的模块(单迭代):`完成数 / 总数 × 100`(取整) +- 有任务卡的模块(多迭代):**只计当前 iteration 的任务卡**,旧迭代 done 任务不计入分母,避免历史完成率虚高 + +## 迭代管理 + +### 何时递增 iteration + +**需要递增**(重构已完成的功能): +- 一个已有 ✅ done 任务卡的模块需要重新设计或重写 +- 核心交互/架构方案被替换(不是在原有基础上扩展) + +**不需要递增**(正常演进): +- 在现有模块上添加新功能 +- 修复 bug +- 新增模块 + +### 重构时的操作步骤 + +1. **manifest.yaml 顶层** `iteration` +1,更新 `updated` 日期 +2. **旧任务卡**:保留原位,在卡片末尾加一行注释说明被替代(不改 status、不删除): + ```markdown + ### ✅ 旧功能实现 + - status: done + - iteration: 1 + ... + > ⚠️ 已被迭代 2「新功能实现」替代,仅作历史记录。 + ``` +3. **新任务卡**:新建替代卡,标注 `iteration` 和 `replaces`: + ```markdown + ### 📋 新功能实现 + - status: todo + - iteration: 2 + - replaces: 旧功能实现 + - complexity: M + - files: ... + - acceptance: ... + ``` +4. **模块文件**:追加 `## 迭代记录` 段落(见下方格式) +5. **模块 status**:重置为 `in_progress`,progress 按当前迭代任务卡重新计算 + +### 迭代记录格式 + +模块文件末尾追加,每次迭代一条: + +```markdown +## 迭代记录 + +### 迭代 2(YYYY-MM-DD) +- **原因**:React Flow 在 50+ 节点时性能卡顿,重构为虚拟化渲染方案 +- **替代**:「React Flow 节点图渲染(迭代1)」→「节点图虚拟化渲染」 +- **保留**:迭代1的任务卡作为历史记录,不删除 +``` + +### 梦核如何感知迭代 + +- usage.json 快照携带 `iteration` 字段(见飞轮外环章节) +- 飞轮数据层按 iteration 分组计算完成率,迭代跳变(如 1→2 后完成率骤降)是正常信号 +- 梦核 prompt 会展示「完成率跳变项目」摘要,Opus 据此判断是主动重构还是项目失控 ## 决策记录格式 @@ -144,15 +214,32 @@ Claude 在每次对话中应遵循以下流程: | "开始做 XX" | 模块 status → in_progress | | "为什么选这个方案" | 记录到模块的 `## 决策记录` | -### 阶段 3:实现功能 — 标记完成或上报阻塞 -``` -成功 → 更新任务卡前缀为 ✅ → status: done -所有任务卡完成 → 模块 status: done -更新 manifest.yaml 的 updated 日期 +### 阶段 3:实现功能 — 两阶段执行锁 + +任务卡执行遵循**两阶段提交**:开始写第一行代码前加锁(信息1),全部完成后解锁(信息2)。只有信息1没有信息2,说明任务被中断。 -失败且可自行解决 → 继续尝试 -失败且无法独立解决 → 标记 🔴 blocked + 填写 blocked_reason → 留给架构师处理 ``` +📋 todo + ↓ 【信息1:加锁】第一行代码改动之前,先更新任务卡 +🔵 in_progress + ↓ 执行代码改动 + ↓ + ├─ 成功 → 【信息2:解锁】更新任务卡前缀为 ✅ → status: done + │ 所有任务卡完成 → 模块 status: done + │ 更新 manifest.yaml 的 updated 日期 + │ + ├─ 失败且可自行解决 → 继续尝试,保持 🔵 + │ + └─ 失败且无法独立解决 → 🔴 blocked + blocked_reason → 留给架构师处理 +``` + +#### 锁悬空时的续接规则 + +新会话读蓝图发现 🔵 任务卡(只有信息1,没有信息2)时: +1. 检查任务卡的 `files` 字段,读取相关文件的**当前代码状态** +2. 判断已完成什么、还缺什么 +3. 从断点继续执行,**不重头来过** +4. 完成后补写 ✅(信息2),关锁 #### 架构师处理 blocked 任务 当 Opus 读蓝图发现 🔴 blocked 任务时: @@ -179,6 +266,74 @@ Claude 应关注这些信号来决定是否更新蓝图: - 执行模型遇到无法解决的问题 → 标记 🔴 blocked + blocked_reason - Opus 看到 blocked 任务 → 分析原因,拆/补/调后产出新任务卡 +## 飞轮外环(跨项目数据反哺) + +内环描述的是单项目的执行流程。外环负责**把多个项目的执行数据聚合起来,反哺 CONVENTIONS 本身**。 + +### 数据采集:usage.json 快照 + +每次 Claude 读取 `.blueprint/manifest.yaml`(通过炼境打开蓝图时触发),炼境自动向项目的 `.blueprint/usage.json` 追加一条当日快照: + +```json +{ + "project_name": "my-project", + "snapshots": [ + { + "at": "2026-04-05T10:00:00Z", + "conventions_version": "1.3.0", + "iteration": 1, + "modules": { "total": 22, "done": 18, "in_progress": 2, "planned": 1, "concept": 1, "blocked": 0, "stalled": [] }, + "tasks": { "total": 45, "done": 38, "in_progress": 3, "blocked": 0, "todo": 4, "dispatched": 3 }, + "blocked_reasons": [] + } + ] +} +``` + +- `dispatched` = 当前满足📋可派发条件的任务数(有 files + acceptance + complexity S/M) +- 节流:同一 UTC 日期只写一条,避免多次打开导致数据膨胀 +- 文件存于项目本地,不上传服务器 + +### 数据聚合:飞轮指标 + +炼境跨项目读取所有 usage.json,按 `conventions_version` 分组,对每个版本队列计算 4 项指标: + +| 指标 | 计算方式 | 含义 | +|------|----------|------| +| 可派发率 | dispatched / total tasks | 任务卡规划质量,越高说明任务定义越清晰 | +| 任务完成率 | done / total tasks | 整体执行进度均值 | +| 飞轮持续率 | 30天内有快照的项目比例 | 外环是否在持续运转 | +| 阻塞密度 | blocked / total tasks | 系统性阻塞程度,越低越好 | + +迭代感知:同一项目若 `iteration` 值增大,说明发生了主动重构。飞轮数据层对此类项目单独计算「当前迭代完成率」,与历史累计值区分展示,避免虚高。梦核 prompt 中标注「完成率跳变项目」供 Opus 分析是健康重构还是项目失控。 + +### 梦核分析:记忆巩固 + +炼境将聚合数据 + 当前 CONVENTIONS 全文格式化为提示词,发给 **Claude Opus** 做两件事: + +- **固化**:哪些跨项目重复出现的阻塞模式、哪些有效实践,值得写进 CONVENTIONS +- **遗忘**:哪些休眠项目(90天无快照)的 AI 文档可以归档,哪些 CONVENTIONS 规则已过时 + +### 闭环路径 + +``` +执行任务 → usage.json 快照 + ↓ + 炼境聚合指标 + ↓ + 梦核提示词 → Claude Opus 分析 + ↓ + 用户/维护者审核建议 + ↓ + 更新 CONVENTIONS.md 源码 + 版本号 + ↓ + 发布新版炼境 + ↓ + 治理面板 sync_blueprint_rules → 分发到各项目 +``` + +> **注意**:CONVENTIONS.md 编译进炼境二进制,更新需要修改炼境源码并发版,不支持运行时修改。这是有意为之的设计——保证规则版本可追溯,防止随意污染。 + ## 注意事项 - 不要删除已完成的任务卡,它们是项目历史的一部分 diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index b22d881..94ed9f5 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -1,7 +1,9 @@ version: 1 -name: Dev Manager Tauri +name: 炼境 description: Git 多空间开发管理器,集成项目管理、Git 操作、CI/CD、开发工具链管理 -updated: 2026-04-03 +iteration: 1 +updated: 2026-04-04 + areas: @@ -83,8 +85,8 @@ modules: - id: blueprint-onboarding name: 蓝图接入引导 area: frontend - status: planned - progress: 0 + status: done + progress: 100 position: [1250, 0] # ── 后端 ──────────────────────────────────────── @@ -140,15 +142,22 @@ modules: - id: blueprint-governance name: 蓝图治理 area: backend - status: planned - progress: 0 + status: done + progress: 100 position: [1250, 350] - - id: blueprint-feedback - name: 蓝图反馈聚合 + - id: blueprint-flywheel + name: 飞轮数据层 area: backend - status: concept - progress: 0 + status: done + progress: 100 + position: [1000, 500] + + - id: blueprint-feedback + name: 梦核 · 记忆巩固 + area: backend + status: done + progress: 100 position: [1250, 500] # ── 基础设施 ──────────────────────────────────── @@ -163,7 +172,7 @@ modules: name: 客户端自动更新 area: infra status: done - progress: 90 + progress: 100 position: [250, 650] - id: wsl-proxy @@ -187,6 +196,34 @@ modules: progress: 100 position: [750, 650] + - id: tray-service + name: 系统托盘常驻 + area: infra + status: done + progress: 100 + position: [1000, 650] + + - id: mcp-server + name: 内嵌 MCP Server + area: backend + status: done + progress: 100 + position: [1500, 350] + + - id: mcp-config-inject + name: MCP 配置注入 + area: backend + status: done + progress: 100 + position: [1500, 500] + + - id: product-group-mcp + name: 产品组 MCP 设置 + area: frontend + status: done + progress: 100 + position: [1500, 0] + edges: # 前端依赖 - from: workspace-mgmt @@ -235,14 +272,21 @@ edges: - from: blueprint-reader to: blueprint-governance type: related - - from: blueprint-view - to: blueprint-onboarding - type: dependency - - from: blueprint-governance - to: blueprint-feedback - type: related - from: blueprint-onboarding to: blueprint-view + type: dependency + # 外环:飞轮数据层 → 梦核 → 治理分发 + - from: blueprint-reader + to: blueprint-flywheel + type: dependency + - from: blueprint-flywheel + to: blueprint-feedback + type: dependency + - from: blueprint-feedback + to: blueprint-governance + type: dependency + - from: blueprint-view + to: blueprint-flywheel type: related # 基础设施关联 @@ -258,3 +302,17 @@ edges: - from: auto-update to: settings-page type: related + + # 产品组 MCP 飞轮 + - from: tray-service + to: mcp-server + type: dependency + - from: mcp-server + to: mcp-config-inject + type: dependency + - from: mcp-config-inject + to: product-group-mcp + type: dependency + - from: product-group-mcp + to: settings-page + type: related diff --git a/.blueprint/modules/auto-update.md b/.blueprint/modules/auto-update.md index c7ee862..7d93f14 100644 --- a/.blueprint/modules/auto-update.md +++ b/.blueprint/modules/auto-update.md @@ -2,7 +2,7 @@ id: auto-update name: 客户端自动更新 status: done -progress: 90 +progress: 100 --- ## 概述 @@ -35,6 +35,6 @@ progress: 90 updater:default + process:default + http 域名放行。 -### 💭 自动检查更新 -- status: concept +### ✅ 自动检查更新 +- status: done - notes: 应用启动时自动检查,发现新版本后在状态栏提示,而非需要手动进入设置页。 diff --git a/.blueprint/modules/blueprint-feedback.md b/.blueprint/modules/blueprint-feedback.md index 8962910..13feb82 100644 --- a/.blueprint/modules/blueprint-feedback.md +++ b/.blueprint/modules/blueprint-feedback.md @@ -1,29 +1,64 @@ --- id: blueprint-feedback -name: 蓝图反馈聚合 -status: concept -progress: 0 +name: 梦核 · 记忆巩固 +status: done +progress: 100 --- ## 概述 -建立跨项目蓝图反馈收集和分析机制。各项目 Claude 执行时将遇到的问题、格式缺口、改进建议写入 `.blueprint/FEEDBACK.md`,dev-manager-tauri 扫描聚合后呈现,供 Opus 分析并优化 master CONVENTIONS.md,形成飞轮闭环。 +炼境的记忆巩固层。类比人类睡眠时大脑对记忆的主动处理——保留重要的(固化进 CONVENTIONS),清理无用的(归档休眠文档)。 -## 决策记录 +梦核不是算法,是一条精心设计的提示词。炼境负责收集和格式化数据(飞轮数据层),Claude 负责做判断:哪些跨项目重复出现的阻塞模式值得写进 CONVENTIONS,哪些长期休眠的文档可以归档。用户确认后,治理模块执行分发。 -- FEEDBACK.md 为纯 Markdown,按条目写入,每条含 type / reason / suggestion 字段 -- 反馈类型:blocked(执行受阻)/ convention_gap(规则不够用)/ format_issue(格式问题)/ improvement(改进建议) -- 聚合视图只读,不提供编辑功能,Claude 才是修改 CONVENTIONS.md 的执行者 +``` +[飞轮数据层] 聚合跨项目数据 + ↓ +[梦核提示词] 喂给 Claude Opus 做判断 + ↓ + 固化 ──────────────────── 遗忘 + ↓ ↓ +CONVENTIONS 改进草稿 归档候选列表 + ↓ ↓ +[治理模块] 分发 用户确认 → 归档操作 +``` ## 任务卡 -### 💭 FEEDBACK.md 格式约定 -- status: concept -- notes: 需要在 CONVENTIONS.md 中加入 FEEDBACK.md 的写入规则和格式规范,确保各项目 Claude 写入格式一致 +### ✅ 梦核分析入口(蓝图弹窗) +- status: done +- complexity: S +- files: src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts +- depends: blueprint-flywheel +- acceptance: 治理面板新增「梦核分析」区块,点击"♻️ 打开飞轮面板"跳转到飞轮面板;飞轮面板生成梦核提示词(可复制);引导文字说明正确流程:「复制 → 粘贴给 Claude Opus → 将建议反馈给炼境维护者 → 更新 CONVENTIONS.md 并发布新版本」(梦核不自动写入,需手动修改源码发版) -### 💭 get_all_blueprint_feedbacks command -- status: concept -- notes: 扫描所有已注册项目的 .blueprint/FEEDBACK.md,解析条目,按 type 分类返回 +### ✅ 归档候选展示 +- status: done +- complexity: S +- files: src/components/dashboard/BlueprintModal.tsx +- depends: blueprint-flywheel +- acceptance: 飞轮面板展示两类归档候选:① 飞轮停转项目(usage.json 最新 at 距今超过 60 天,显示最后活跃日期)② AI文档休眠项目(usage.json 最新 at 距今超过 90 天,推断 Claude 长期未读该项目的 AI 文档);每条候选显示项目名 + 最后活跃日期 + 「归档」按钮 -### 💭 跨项目反馈聚合视图 +### ✅ 归档 command +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- acceptance: archive_project_docs command;接收项目路径;将 .blueprint/modules/*.md 复制到 .blueprint/archive/YYYY-MM-DD-HHmmss/ 后删除原文件;写 MANIFEST.txt 记录归档信息;返回归档摘要(archived_files + archive_dir) + +### 💭 恢复 command - status: concept -- notes: 在蓝图界面或设置页展示:高频 blocked 原因、重复出现的 convention_gap、跨项目 suggestion 列表;提供「复制给 Opus 分析」按钮,生成汇总报告 +- notes: 读取 .blueprint/archive/*/MANIFEST.txt,将文件还原到原路径,支持选择性恢复,覆盖前需用户确认 + +### ✅ 梦核提示词增加停滞模块数据 +- status: done +- complexity: S +- files: src-tauri/src/commands/blueprint.rs, src/lib/commands.ts +- acceptance: get_flywheel_stats 收集所有项目最新快照的 modules.stalled,聚合为 stalled_modules: Vec(格式 "project/module_id");FlywheelStats 新增 stalled_modules 字段;梦核提示词新增「停滞模块」摘要段;同步实现迭代跳变检测(iteration_jumps)并补入提示词 + +## 决策记录 + +- 梦核的核心是提示词工程而非算法,判断质量随 Claude 模型升级自动变强,无需维护打分公式 +- 固化输出(CONVENTIONS 改进)由用户确认后交 governance 执行,不自动写入,保留人工节点 +- 遗忘操作(归档)提供撤销能力(MANIFEST.txt + 恢复 command),降低误操作风险 +- AI 文档休眠判断基于 usage.json 中 get_blueprint 的调用记录,间接反映 Claude 是否在读这些文档 +- 命名选「梦核」:梦境处理记忆(非主动学习),核心是巩固而非监控 +- 归档功能直接操作 .blueprint/modules/,用户在飞轮面板触发,清空休眠项目的模块 md 文件 diff --git a/.blueprint/modules/blueprint-flywheel.md b/.blueprint/modules/blueprint-flywheel.md new file mode 100644 index 0000000..6b2e3f5 --- /dev/null +++ b/.blueprint/modules/blueprint-flywheel.md @@ -0,0 +1,55 @@ +--- +id: blueprint-flywheel +name: 飞轮数据层 +status: done +progress: 100 +--- + +## 概述 +外环数据采集与聚合层。在各项目的 .blueprint/usage.json 中记录飞轮运转快照(任务完成率、阻塞密度、conventions 版本),炼境跨项目聚合后计算健康指标,并格式化为梦核分析提示词的输入数据。 + +代码只处理确定性的机械工作(采集、计算、格式化),判断交给 Claude(梦核)。 + +## 任务卡 + +### ✅ usage.json 埋点写入 +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs +- acceptance: get_blueprint command 执行后,向项目 .blueprint/usage.json 追加一条 snapshot;schema:{ project_name, at, conventions_version, iteration, modules: {total,done,in_progress,planned,concept,blocked,stalled:string[]}, tasks: {total,done,in_progress,blocked,todo,dispatched}, blocked_reasons: string[] };iteration 从 manifest.yaml 顶层 iteration 字段读取,失败写 1;节流:同一 UTC 日期同一项目只追加一条;conventions_version 从 .blueprint/CONVENTIONS.md frontmatter 读取,失败写 "unknown";usage.json 不存在时自动创建为 { "project_name": "...", "snapshots": [] } + +### ✅ get_flywheel_stats command +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- depends: blueprint-reader +- acceptance: 接收项目路径数组(由前端从 store 传入,与 sync_blueprint_rules 同模式);读取每个项目的 .blueprint/usage.json;按 conventions_version 分组,对每组计算 4 项指标:可派发率(dispatched/total tasks)、任务完成率(done/total tasks)、飞轮持续率(30天内有快照的项目比例)、阻塞密度(blocked/total tasks);跨项目聚合 blocked_reasons TOP10;检测停转项目(60天无快照)和 AI文档休眠项目(90天无快照),均携带 last_active 日期;返回结构化 FlywheelStats + +### ✅ get_flywheel_stats 支持迭代感知 +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src/lib/commands.ts +- acceptance: usage.json snapshot 写入 iteration(读 manifest.yaml 顶层字段,失败写 1);BlueprintManifest struct 新增 iteration 字段;get_flywheel_stats 扫描每个项目所有快照,检测 iteration 跳变,返回 iteration_jumps: Vec;同时收集最新快照中 modules.stalled 列表,返回 stalled_modules: Vec(格式 "project/module_id");梦核 prompt 补充「迭代跳变项目」和「停滞模块」两段摘要;commands.ts 新增 IterationJump 接口,FlywheelStats 补两字段 + +### ✅ 梦核提示词生成 command +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- depends: blueprint-reader +- acceptance: generate_muhe_prompt command,接收 FlywheelStats(由前端先调 get_flywheel_stats 获取后传入)+ 读取 master CONVENTIONS.md 内容,生成完整梦核分析提示词字符串;提示词包含:聚合数据摘要、TOP blocked_reasons、CONVENTIONS 全文、固化/遗忘双任务指令;注意:本任务须在 get_flywheel_stats 实现后执行 + +### ✅ 飞轮健康面板 +- status: done +- complexity: M +- files: src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts +- depends: blueprint-view +- acceptance: 蓝图弹窗新增「飞轮」侧边面板;展示:各 conventions 版本队列的指标对比表(可派发率 / 任务完成率 / 飞轮持续率 / 阻塞密度)、跨项目 blocked_reasons TOP10、飞轮停转项目(60–90天,仅展示未进入休眠列表的)、AI文档休眠项目(90天+,含归档按钮+二次确认弹窗)、迭代跳变项目(iteration 递增,展示完成率跳变,⚠️标注大幅下降)、停滞模块(in_progress 但无可推进任务,格式 project/module_id);「生成梦核提示词」按钮调用 generate_muhe_prompt,展示结果供复制 + +## 决策记录 + +- 代码只做机械工作(采集、聚合、格式化),梦核判断完全交给 Claude,不写打分公式 +- usage.json 存各项目本地,不上传服务器,符合炼境 document-driven 设计哲学 +- 节流策略:同一天一条,避免频繁打开蓝图导致数据膨胀 +- conventions_version 打标是版本对比的关键,是外环版本评估的基础 +- 4项指标中「可派发率」衡量任务卡规划质量,「飞轮持续率」衡量外环是否在转,两者是最先导指标;任务完成率衡量整体执行进度,阻塞密度是系统性风险信号 +- iteration 字段用于区分历史 done 和当前迭代 done,避免多次重构后完成率虚高;迭代跳变是正常信号(主动重构),梦核据此判断而非告警 diff --git a/.blueprint/modules/blueprint-governance.md b/.blueprint/modules/blueprint-governance.md index 1be7846..53bf461 100644 --- a/.blueprint/modules/blueprint-governance.md +++ b/.blueprint/modules/blueprint-governance.md @@ -1,8 +1,8 @@ --- id: blueprint-governance name: 蓝图治理 -status: planned -progress: 0 +status: done +progress: 100 --- ## 概述 @@ -13,36 +13,37 @@ progress: 0 - CONVENTIONS.md 加 YAML frontmatter 版本号,应用以此判断是否需要同步 - CLAUDE.md 用 `` / `` 标记管理区,应用只替换标记内内容,不动项目自有内容 - 无 CLAUDE.md 时生成初始文件;有 CLAUDE.md 但无 MANAGED 区时追加到末尾 +- MASTER_CONVENTIONS 通过 `include_str!()` 在编译时嵌入,保证 binary 自包含 +- BlueprintStatus 四状态:none / rules_outdated / content_stale / synced ## 任务卡 -### 📋 CONVENTIONS.md 版本化 -- status: todo +### ✅ CONVENTIONS.md 版本化 +- status: done - complexity: S - files: .blueprint/CONVENTIONS.md - acceptance: frontmatter 加 version 和 updated 字段,格式为 semver(如 1.0.0) -### 📋 get_blueprint_status command -- status: todo -- complexity: M -- files: src-tauri/src/commands/blueprint.rs -- acceptance: 接收项目路径,返回四种状态之一:none / rules_outdated / content_stale / synced;判断依据:.blueprint/ 是否存在、CONVENTIONS.md 版本是否匹配 master、CLAUDE.md 是否有 MANAGED 区且版本匹配 - -### 📋 sync_blueprint_rules command -- status: todo -- complexity: M -- files: src-tauri/src/commands/blueprint.rs -- acceptance: 接收项目路径,执行三步:(1) 覆盖写入最新 CONVENTIONS.md;(2) 检测 CLAUDE.md 是否存在及 MANAGED 区状态,按三种情况(无文件/无区块/版本旧)分别处理;(3) 返回操作摘要 - -### 📋 CLAUDE.md 模板定义 -- status: todo +### ✅ CLAUDE.md 模板定义 +- status: done - complexity: S - files: src-tauri/src/commands/blueprint.rs - acceptance: 在 Rust 中定义 CLAUDE.md MANAGED 区的标准内容模板,内容涵盖:蓝图飞轮流程、读取 manifest 指令、任务卡派发规则;模板版本与 CONVENTIONS.md 同步 -### 📋 批量同步所有项目规则 -- status: todo +### ✅ get_blueprint_status command +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- acceptance: 接收项目路径,返回四种状态之一:none / rules_outdated / content_stale / synced;判断依据:.blueprint/ 是否存在、CONVENTIONS.md 版本是否匹配 master、CLAUDE.md 是否有 MANAGED 区且版本匹配 + +### ✅ sync_blueprint_rules command +- status: done +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- acceptance: 接收项目路径,执行三步:(1) 覆盖写入最新 CONVENTIONS.md;(2) 检测 CLAUDE.md 是否存在及 MANAGED 区状态,按三种情况(无文件/无区块/版本旧)分别处理;(3) 返回操作摘要 + +### ✅ 批量同步所有项目规则 +- status: done - complexity: S -- files: src-tauri/src/commands/blueprint.rs, src/components/settings/SettingsPage.tsx -- depends: sync_blueprint_rules command +- files: src/components/settings/SettingsPage.tsx - acceptance: 设置页提供「同步所有项目蓝图规则」按钮,调用 sync_blueprint_rules 遍历所有已注册项目,返回每个项目的同步结果摘要 diff --git a/.blueprint/modules/blueprint-onboarding.md b/.blueprint/modules/blueprint-onboarding.md index 4bd5be8..e83788d 100644 --- a/.blueprint/modules/blueprint-onboarding.md +++ b/.blueprint/modules/blueprint-onboarding.md @@ -1,8 +1,8 @@ --- id: blueprint-onboarding name: 蓝图接入引导 -status: planned -progress: 0 +status: done +progress: 100 --- ## 概述 @@ -13,26 +13,26 @@ progress: 0 - 提示词生成由 Rust 完成(扫描目录结构、读取 git log、拼接模板),前端只做展示 - 提示词包含完整上下文(项目信息 + 目录结构 + 现有蓝图 + CONVENTIONS 摘要),确保 Claude 无需额外询问 - 目录扫描限深度 3 层,避免输出过大 +- 配置文件扫描采用「存在则读」策略:AI 文档(ARCHITECTURE.md/CLAUDE.md/AGENTS.md)自动包含,对无此类文件的简单项目零影响 +- monorepo 自动感知:检测到 pnpm-workspace.yaml 等标志文件后,自动读子包配置并调整模块数建议,用户无需手动区分项目复杂度 ## 任务卡 -### 📋 generate_blueprint_prompt command -- status: todo +### ✅ generate_blueprint_prompt command +- status: done - complexity: M - files: src-tauri/src/commands/blueprint.rs - depends: blueprint-governance -- acceptance: 接收项目路径和提示词类型(init / sync),返回对应提示词字符串;init 类型扫描目录结构 + 读取 README/package.json/Cargo.toml;sync 类型额外读取现有蓝图文件 + 近 20 条 git log +- acceptance: 接收项目路径和提示词类型(init / sync),返回对应提示词字符串;init 类型扫描目录结构 + 读取 README/package.json/Cargo.toml/ARCHITECTURE.md/CLAUDE.md/AGENTS.md/DEVELOPMENT.md(存在才读,不存在跳过);monorepo 自动检测(pnpm-workspace.yaml/lerna.json/turbo.json),检测到则额外读 apps/ 前4个子包 + packages/ 前2个子包的 package.json,并在提示词中注入 monorepo 提示(模块数建议 10-25、以业务能力为单位划分);sync 类型额外读取现有蓝图文件 + 近 20 条 git log -### 📋 项目列表蓝图状态徽章 -- status: todo +### ✅ 项目列表蓝图状态徽章 +- status: done - complexity: M - files: src/components/dashboard/ProjectCard.tsx -- depends: get_blueprint_status command - acceptance: 项目卡片显示蓝图状态徽章(🔘无蓝图 / 🟡规则待更新 / 🟠内容待同步 / 🟢已同步);🟡状态点击直接执行同步(无需 Claude);其余状态点击打开操作弹窗 -### 📋 蓝图操作弹窗 -- status: todo +### ✅ 蓝图操作弹窗 +- status: done - complexity: M - files: src/components/dashboard/BlueprintPromptModal.tsx -- depends: generate_blueprint_prompt command - acceptance: 弹窗展示生成的提示词(monospace、可选中)、一键复制按钮、说明文案("复制后粘贴给 Claude Opus 执行");🔘状态显示「初始化蓝图」提示词,🟠状态显示「同步描绘」提示词 diff --git a/.blueprint/modules/blueprint-view.md b/.blueprint/modules/blueprint-view.md index c5c108d..c6b67c3 100644 --- a/.blueprint/modules/blueprint-view.md +++ b/.blueprint/modules/blueprint-view.md @@ -1,8 +1,8 @@ --- id: blueprint-view name: 蓝图可视化 -status: done -progress: 100 +status: in_progress +progress: 86 --- ## 概述 @@ -78,6 +78,30 @@ progress: 100 - files: src/components/dashboard/BlueprintModal.tsx - acceptance: 根据模块相对位置自动选择最短路径 handle,避免连线绕远 +### ✅ 蓝图治理面板 +- status: done +- complexity: M +- files: src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts +- acceptance: 侧边新增「治理」面板,展示 CONVENTIONS.md 当前版本与 master 版本对比;提供「同步规则」按钮(替代设置页的同步入口);提供「初始化/同步蓝图」提示词入口(替代卡片徽章弹窗,统一收口) + +### ✅ 顶部状态行扩展 +- status: done +- complexity: S +- files: src/components/dashboard/BlueprintModal.tsx +- depends: blueprint-governance +- acceptance: 统计条新增「规则」状态指示(🟢已同步/🟡待更新),点击跳转到治理面板;「AI文档」指示器预留位置但灰显(depends: blueprint-feedback 未完成时不可用,避免跳转到不存在的面板) + +### ✅ 批量执行提示词入口 +- status: done +- complexity: S +- files: src/components/dashboard/BlueprintModal.tsx +- depends: blueprint-view +- acceptance: 下一步汇总面板新增「复制批量执行提示词」按钮;点击后将当前项目的批量执行提示词复制到剪贴板;提示词包含项目名称,指示 Claude 自主遍历所有 📋 任务卡依次执行(标记 🔵 加锁 → 实现 → 标记 ✅ 解锁,失败标 🔴 blocked),中途不等用户确认,全部完成后输出执行报告;提示词纯前端生成,无需新增 Tauri command + +### 💭 AI 文档健康面板 +- status: concept +- notes: 侧边新增「AI文档」面板,列出项目内所有 AI 相关文档(CLAUDE.md/AGENTS.md/.cursorrules 等)及其使用频率评分;标出休眠文档;提供「归档」操作。依赖 ai-doc-analytics 模块的埋点数据。 + ## 决策记录 - 选择 React Flow (@xyflow/react) 而非 D3 或自绘 canvas,因为 React Flow 提供开箱即用的节点拖拽、缩放、连线路由 @@ -85,3 +109,4 @@ progress: 100 - 区域分组用 React Flow 父子节点实现,模块作为子节点自动跟随分组 - hover 只做边高亮,点击(pin)才做节点淡化,避免 React Flow 节点重建导致的闪烁 - 飞轮模型分配:Opus 做架构需求和蓝图维护,Sonnet 执行任务卡,.blueprint/ 文件作为模型间共享状态 +- 蓝图弹窗定位为项目蓝图驾驶舱:所有蓝图相关操作(治理、AI文档健康、提示词生成)统一收口到此弹窗,不再分散到设置页和卡片徽章 diff --git a/.blueprint/modules/mcp-config-inject.md b/.blueprint/modules/mcp-config-inject.md new file mode 100644 index 0000000..f3f108d --- /dev/null +++ b/.blueprint/modules/mcp-config-inject.md @@ -0,0 +1,40 @@ +# MCP 配置注入 + +产品组成员变更时,炼境自动向对应项目的 `.claude/settings.json` 注入或移除 MCP 服务器配置,实现热插拔。 + +## 决策记录 + +- 使用 `lian-jing:group-{group_id}` 命名空间前缀标记炼境写入的 key,与用户自有配置严格隔离 +- 炼境只操作自己命名空间下的 key,其余配置永远不动 +- 加入/退出操作由炼境 UI 驱动,炼境必然在线,无需异步清理机制 +- 注入前做 JSON 有效性校验,文件损坏时不覆盖,在 UI 提示用户 + +## 任务卡 + +### ✅ 加入产品组时注入配置 +- status: done +- complexity: M +- files: src-tauri/src/mcp_inject.rs, src-tauri/src/commands/groups.rs +- depends: mcp-server +- acceptance: + - 项目加入产品组后,其 win_path 或 wsl_path 对应的 `.claude/settings.json` 被写入以下条目 + - key 格式:`lian-jing:group-{group_id}` + - value:`{ "type": "http", "url": "http://localhost:{port}/mcp/group/{group_id}" }` + - 文件不存在时自动创建;文件已存在时合并写入,不影响其他 key + - 文件 JSON 损坏时 eprintln 记录,不影响成员关系 DB 写入 + +### ✅ 退出产品组时清除配置 +- status: done +- complexity: S +- files: src-tauri/src/mcp_inject.rs, src-tauri/src/commands/groups.rs +- depends: mcp-config-inject(注入) +- acceptance: + - 项目退出产品组后,`lian-jing:group-{group_id}` 这一 key 被精确删除 + - 其他 key(包括其他产品组的 key、用户自有配置)完全不受影响 + - 在 DB 删除前先执行清除(remove_project_mcp 需要 project_workspaces 查路径) + +### ✅ 端口变更时重新注入所有配置 +- status: done +- complexity: S +- files: src-tauri/src/mcp_inject.rs, src-tauri/src/commands/settings.rs +- acceptance: update_settings 检测到 mcp_port key 变更后自动调用 reinject_all_mcp,遍历所有 project_group_map 记录重新写入 settings.json diff --git a/.blueprint/modules/mcp-server.md b/.blueprint/modules/mcp-server.md new file mode 100644 index 0000000..10178e4 --- /dev/null +++ b/.blueprint/modules/mcp-server.md @@ -0,0 +1,41 @@ +# 内嵌 MCP Server + +炼境进程内置一个 axum HTTP 服务器,实现 MCP 协议,按产品组聚合上下文数据,供组内所有项目的 Claude 实例查询。 + +## 决策记录 + +- 内嵌在 Tauri 进程而非独立进程,因为炼境托盘常驻时天然保证服务存活,无需额外进程管理 +- 使用 HTTP/SSE 传输而非 stdio,因为 HTTP 可被 Windows 和 WSL 两侧同时访问(WSL localhost 穿透) +- 默认端口 27190(冷门端口,冲突概率极低),可在设置页修改 +- MCP 工具按 group_id 路由,不同产品组的数据完全隔离 + +## 任务卡 + +### ✅ 启动时绑定 HTTP 端口 +- status: done +- complexity: M +- files: src-tauri/src/mcp_server.rs, src-tauri/src/lib.rs, src-tauri/Cargo.toml +- depends: tray-service +- acceptance: 炼境启动后 `http://localhost:27190/health` 返回 200,进程退出后端口释放 + +### ✅ 实现 MCP JSON-RPC 基础协议 +- status: done +- complexity: M +- files: src-tauri/src/mcp_server.rs +- acceptance: 支持 `initialize`、`tools/list`、`tools/call` 三个 JSON-RPC 方法,通过 MCP Inspector 验证 + +### ✅ 实现产品组上下文工具 +- status: done +- complexity: M +- files: src-tauri/src/mcp_server.rs +- depends: mcp-server(基础协议) +- acceptance: 以下三个工具可用且返回正确数据 + - `list_group_projects(group_id)` → 组内项目列表(名称、路径、平台、tech_stack) + - `get_project_blueprint(group_id, project_id)` → 对应项目的 manifest.yaml 内容 + - `get_project_file(group_id, project_id, relative_path)` → 对应项目的任意文件内容 + +### ✅ MCP 端口可配置(Rust 侧) +- status: done +- complexity: S +- files: src-tauri/src/db.rs, src-tauri/src/mcp_server.rs +- acceptance: settings 表默认写入 mcp_port=27190,启动时读取该值绑定端口(SettingsPage UI 见 product-group-mcp 模块) diff --git a/.blueprint/modules/product-group-mcp.md b/.blueprint/modules/product-group-mcp.md new file mode 100644 index 0000000..b1092f0 --- /dev/null +++ b/.blueprint/modules/product-group-mcp.md @@ -0,0 +1,25 @@ +# 产品组 MCP 设置 + +产品组 UI 中展示 MCP 激活状态,让用户可感知哪些项目已接入跨项目上下文能力。 + +## 任务卡 + +### ✅ 产品组详情显示 MCP 注入状态 +- status: done +- complexity: S +- files: src/components/manage/ManagePage.tsx +- depends: mcp-config-inject +- acceptance: + - 产品组成员卡片显示平台标识(Win / WSL) + - 已配置路径的项目显示绿点 "MCP" 徽标,hover 显示路径 + - 未配置路径的项目显示「无路径」灰色徽标,说明注入已跳过 + +### ✅ 设置页添加 MCP 端口配置 +- status: done +- complexity: S +- files: src/components/settings/SettingsPage.tsx, src-tauri/src/commands/settings.rs +- depends: mcp-server(端口可配置) +- acceptance: + - 设置页新增「MCP 服务」分区,含端口输入框(默认 27190)和绿色「运行中」状态 + - 保存时校验端口范围(1024~65535),调用 updateSettings({mcp_port}) + - Rust 侧自动触发 reinject_all_mcp,所有成员配置即时更新 diff --git a/.blueprint/modules/tray-service.md b/.blueprint/modules/tray-service.md new file mode 100644 index 0000000..0dbe435 --- /dev/null +++ b/.blueprint/modules/tray-service.md @@ -0,0 +1,28 @@ +# 系统托盘常驻 + +炼境作为后台服务常驻,关闭窗口后进程继续运行,保持 MCP Server 持续提供服务。 + +## 决策记录 + +- 选择托盘常驻而非动态端口,因为 MCP 配置注入写静态 URL,进程不常驻则 Claude 连不上 +- 使用 tauri-plugin-single-instance 保证进程唯一,从根本上消除端口冲突风险 + +## 任务卡 + +### ✅ 集成 tauri-plugin-single-instance +- status: done +- complexity: S +- files: src-tauri/Cargo.toml, src-tauri/src/lib.rs, src-tauri/tauri.conf.json +- acceptance: 双击启动炼境时不新建进程,自动激活已有窗口并置顶 + +### ✅ 系统托盘图标与菜单 +- status: done +- complexity: S +- files: src-tauri/src/lib.rs, src-tauri/tauri.conf.json +- acceptance: 任务栏托盘显示炼境图标,右键菜单有「显示窗口」「退出炼境」两项 + +### ✅ 关闭窗口最小化到托盘 +- status: done +- complexity: S +- files: src-tauri/src/lib.rs +- acceptance: 点击窗口关闭按钮时,窗口隐藏但进程不退出;点「退出炼境」才真正结束进程 diff --git a/.blueprint/usage.json b/.blueprint/usage.json new file mode 100644 index 0000000..081436a --- /dev/null +++ b/.blueprint/usage.json @@ -0,0 +1,26 @@ +{ + "project_name": "dev-manager-tauri", + "snapshots": [ + { + "at": "2026-04-03T04:52:50Z", + "blocked_reasons": [], + "conventions_version": "1.1.0", + "modules": { + "blocked": 0, + "concept": 0, + "done": 22, + "in_progress": 1, + "planned": 2, + "total": 25 + }, + "tasks": { + "blocked": 0, + "dispatched": 4, + "done": 84, + "in_progress": 0, + "todo": 7, + "total": 95 + } + } + ] +} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f1f42f..698114f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} - name: Dev Manager ${{ github.ref_name }} + name: 炼境 ${{ github.ref_name }} body: | ## 更新内容 请查看提交记录了解详情。 @@ -69,7 +69,7 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} with: tagName: ${{ github.ref_name }} - releaseName: Dev Manager ${{ github.ref_name }} + releaseName: 炼境 ${{ github.ref_name }} releaseBody: | ## 更新内容 请查看提交记录了解详情。 diff --git a/CLAUDE.md b/CLAUDE.md index 3c5e2c9..9413df6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Dev Manager Tauri +# 炼境 (Dev Manager Tauri) Git 多空间开发管理器,Tauri v2 + React + TypeScript + Rust。 @@ -42,3 +42,28 @@ src-tauri/src/commands/ Tauri command 实现 .blueprint/ 项目蓝图(manifest + 模块详情 + 任务卡) docs/ 部署和更新指南 ``` + + +## 项目蓝图 + +本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 + +**每次对话都应遵循以下流程:** + +### 1. 了解当前状态 +对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。 + +### 2. 讨论需求时更新蓝图 +- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/.md` +- 拆分了大需求 → 在模块文件中添加任务卡 +- 调整了方向 → 更新模块关系(edges) + +### 3. 完成实现后标记蓝图 +- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅) +- 更新 manifest.yaml 顶部的 `updated` 日期 + +### 4. 任务卡规范 +详见 `.blueprint/CONVENTIONS.md`,核心规则: +- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发 +- 可派发的任务卡可以直接交给 Claude Agent 独立完成 + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c5b4f53..3d80f41 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -234,6 +234,61 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.21.7" @@ -827,6 +882,7 @@ dependencies = [ name = "dev-manager-tauri" version = "0.1.11" dependencies = [ + "axum", "base64 0.22.1", "chrono", "git2", @@ -845,7 +901,9 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tauri-plugin-updater", + "tokio", "ureq", "urlencoding", "uuid", @@ -1780,6 +1838,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -1794,6 +1858,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -2406,6 +2471,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.8.0" @@ -4050,6 +4121,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -4792,6 +4874,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc61e4822b8f74d68278e09161d3e3fdd1b14b9eb781e24edccaabf10c420e8c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.10.0" @@ -5222,6 +5319,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -5260,6 +5358,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01a22bb..694bf62 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,9 @@ git2 = { version = "0.19", features = ["vendored-openssl"] } tauri-plugin-http = "2" tauri-plugin-updater = "2" tauri-plugin-process = "2" +tauri-plugin-single-instance = "2" +axum = "0.7" +tokio = { version = "1", features = ["net"] } ureq = { version = "2", features = ["json"] } open = "5" base64 = "0.22" diff --git a/src-tauri/src/commands/blueprint.rs b/src-tauri/src/commands/blueprint.rs index 556fe4c..a4d1255 100644 --- a/src-tauri/src/commands/blueprint.rs +++ b/src-tauri/src/commands/blueprint.rs @@ -1,6 +1,37 @@ use serde::{Deserialize, Serialize}; use std::path::Path; +// ── 主节点:master CONVENTIONS.md 内容和版本(编译时嵌入)──────────────────── + +const MASTER_CONVENTIONS: &str = include_str!("../../../.blueprint/CONVENTIONS.md"); +const MASTER_CONVENTIONS_VERSION: &str = "1.3.0"; + +// CLAUDE.md 蓝图管理区模板(版本与 CONVENTIONS 保持一致) +const CLAUDE_MANAGED_TEMPLATE: &str = r#" +## 项目蓝图 + +本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 + +**每次对话都应遵循以下流程:** + +### 1. 了解当前状态 +对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。 + +### 2. 讨论需求时更新蓝图 +- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/.md` +- 拆分了大需求 → 在模块文件中添加任务卡 +- 调整了方向 → 更新模块关系(edges) + +### 3. 完成实现后标记蓝图 +- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅) +- 更新 manifest.yaml 顶部的 `updated` 日期 + +### 4. 任务卡规范 +详见 `.blueprint/CONVENTIONS.md`,核心规则: +- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发 +- 可派发的任务卡可以直接交给 Claude Agent 独立完成 +"#; + // ── manifest.yaml 对应结构 ─────────────────────────────────────────────────── #[derive(Debug, Serialize, Deserialize, Clone)] @@ -9,6 +40,8 @@ pub struct BlueprintManifest { pub name: String, pub description: Option, pub updated: Option, + /// 当前迭代号(默认 1,重构已完成模块时手动递增) + pub iteration: Option, #[serde(default)] pub areas: Vec, #[serde(default)] @@ -151,6 +184,9 @@ pub fn get_blueprint(project_path: String) -> Result, Stri // 统计 let stats = compute_stats(&manifest); + // 飞轮快照(每日一次,静默写入,不影响主流程) + append_usage_snapshot(&bp_dir, &manifest, &stats); + Ok(Some(BlueprintData { manifest, stats })) } @@ -301,7 +337,9 @@ pub fn generate_blueprint_prompt( project_path: String, prompt_type: String, // "init" | "sync" ) -> Result { - let root = Path::new(&project_path); + // 去除尾部斜杠,避免 file_name() 返回 None + let path_str = project_path.trim_end_matches(['/', '\\']).to_string(); + let root = Path::new(&path_str); // 基础信息 let project_name = root @@ -311,42 +349,210 @@ pub fn generate_blueprint_prompt( .to_string(); let dir_tree = scan_dir_tree(root, 0, 3); - let readme = read_file_short(root, "README.md", 60); - let pkg_json = read_file_short(root, "package.json", 40); - let cargo_toml = read_file_short(root, "Cargo.toml", 40); + let tech_stack = detect_tech_stack(root); + let config_files = read_config_files(root); - let tech_hint = detect_tech_stack(root); + let is_monorepo = root.join("pnpm-workspace.yaml").exists() + || root.join("lerna.json").exists() + || root.join("turbo.json").exists(); match prompt_type.as_str() { - "init" => Ok(build_init_prompt(&project_name, &tech_hint, &dir_tree, &readme, &pkg_json, &cargo_toml)), + "init" => { + let git_summary = get_git_summary(root); + // monorepo:补充子包 package.json + let subpackages = if is_monorepo { + read_monorepo_subpackages(root) + } else { + String::new() + }; + let full_config = if subpackages.is_empty() { + config_files + } else { + format!("{config_files}\n\n## 子包配置\n{subpackages}") + }; + Ok(build_init_prompt(&project_name, &tech_stack, &dir_tree, &full_config, &git_summary, is_monorepo)) + } "sync" => { let git_log = get_git_log(root); let blueprint_summary = read_blueprint_summary(root); - Ok(build_sync_prompt(&project_name, &tech_hint, &dir_tree, &git_log, &blueprint_summary)) + Ok(build_sync_prompt(&project_name, &tech_stack, &dir_tree, &config_files, &git_log, &blueprint_summary)) } _ => Err(format!("未知 prompt_type: {prompt_type},应为 init 或 sync")), } } +/// 通过文件存在性检测技术栈(不读 AI 文档内容,避免被旧描述干扰) fn detect_tech_stack(root: &Path) -> String { - let mut hints = Vec::new(); - if root.join("package.json").exists() { - // 尝试从 package.json 读取框架信息 - if let Ok(content) = std::fs::read_to_string(root.join("package.json")) { - for kw in &["next", "react", "vue", "svelte", "vite", "tauri", "electron"] { - if content.contains(kw) { hints.push(kw.to_string()); } + let mut found: std::collections::HashSet<&str> = std::collections::HashSet::new(); + + // 对给定目录做一次检测,结果写入 found + let mut scan_dir = |dir: &Path| { + let cfgs = ["js", "ts", "mjs", "cjs"]; + let has_cfg = |name: &str| cfgs.iter().any(|ext| dir.join(format!("{name}.{ext}")).exists()); + + if has_cfg("next.config") { found.insert("Next.js"); } + if has_cfg("nuxt.config") { found.insert("Nuxt"); } + if has_cfg("svelte.config") { found.insert("SvelteKit"); } + if has_cfg("remix.config") { found.insert("Remix"); } + if has_cfg("astro.config") { found.insert("Astro"); } + if has_cfg("vite.config") { found.insert("Vite"); } + if has_cfg("webpack.config") { found.insert("Webpack"); } + if has_cfg("tailwind.config") { found.insert("TailwindCSS"); } + + if dir.join("tsconfig.json").exists() || dir.join("tsconfig.base.json").exists() { + found.insert("TypeScript"); + } + if dir.join("Cargo.toml").exists() { found.insert("Rust"); } + if dir.join("go.mod").exists() { found.insert("Go"); } + if dir.join("pyproject.toml").exists() + || dir.join("requirements.txt").exists() { found.insert("Python"); } + if dir.join("pom.xml").exists() { found.insert("Java/Maven"); } + if dir.join("build.gradle").exists() + || dir.join("build.gradle.kts").exists() { found.insert("Java/Gradle"); } + if dir.join("prisma").join("schema.prisma").exists() { found.insert("Prisma"); } + let drizzle = ["drizzle.config.ts", "drizzle.config.js", "drizzle.config.mjs"]; + if drizzle.iter().any(|f| dir.join(f).exists()) { found.insert("Drizzle"); } + + // 读 package.json 的 dependencies 字段(仅用于子应用补充框架识别) + let pkg = dir.join("package.json"); + if pkg.exists() { + if let Ok(content) = std::fs::read_to_string(&pkg) { + for (kw, label) in &[("\"vue\"", "Vue"), ("\"react\"", "React"), + ("\"hono\"", "Hono"), ("\"fastify\"", "Fastify"), + ("\"express\"", "Express"), ("\"nestjs\"", "NestJS"), + ("\"@nestjs/core\"", "NestJS")] { + if content.contains(kw) { found.insert(label); } + } + } + } + }; + + // ── 包管理器 / monorepo ────────────────────────────────────────────────── + let mut hints: Vec = Vec::new(); + let is_monorepo = root.join("pnpm-workspace.yaml").exists(); + if is_monorepo { hints.push("pnpm monorepo".into()); } + else if root.join("pnpm-lock.yaml").exists() { hints.push("pnpm".into()); } + else if root.join("bun.lockb").exists() || root.join("bun.lock").exists() { hints.push("Bun".into()); } + else if root.join("yarn.lock").exists() { hints.push("Yarn".into()); } + if root.join("turbo.json").exists() { hints.push("Turborepo".into()); } + + // ── 桌面 ────────────────────────────────────────────────────────────────── + if root.join("src-tauri").join("Cargo.toml").exists() { hints.push("Tauri".into()); } + if root.join("electron-builder.json").exists() + || root.join("electron-builder.yml").exists() { hints.push("Electron".into()); } + + // ── 根目录扫描 ──────────────────────────────────────────────────────────── + scan_dir(root); + + // ── monorepo:扫描 apps/* 和 packages/* 一级子目录 ──────────────────────── + for workspace_dir in &["apps", "packages"] { + let ws = root.join(workspace_dir); + if let Ok(entries) = std::fs::read_dir(&ws) { + for entry in entries.flatten() { + if entry.path().is_dir() { + scan_dir(&entry.path()); + } } } } - if root.join("Cargo.toml").exists() { hints.push("Rust".to_string()); } - if root.join("go.mod").exists() { hints.push("Go".to_string()); } - if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() { - hints.push("Python".to_string()); - } - if root.join("pom.xml").exists() { hints.push("Java/Maven".to_string()); } + + // ── 源码文件扩展名抽样(检测 Vue / React / Svelte)──────────────────────── + detect_ui_from_src(root, &mut found); + + hints.extend(found.into_iter().map(|s| s.to_string())); + hints.sort(); if hints.is_empty() { "未知".to_string() } else { hints.join(", ") } } +/// 抽样 src/ 目录(及 apps/*/src/)判断 UI 框架 +fn detect_ui_from_src(root: &Path, found: &mut std::collections::HashSet<&'static str>) { + let src_dirs: Vec = { + let mut dirs = vec![root.join("src")]; + // monorepo apps + if let Ok(entries) = std::fs::read_dir(root.join("apps")) { + for e in entries.flatten() { + dirs.push(e.path().join("src")); + } + } + dirs + }; + + for src in src_dirs { + if !src.is_dir() { continue; } + if let Ok(entries) = std::fs::read_dir(&src) { + for entry in entries.flatten().take(40) { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.ends_with(".vue") { found.insert("Vue"); } + if name_str.ends_with(".tsx") || name_str.ends_with(".jsx") { found.insert("React"); } + if name_str.ends_with(".svelte") { found.insert("Svelte"); } + } + } + } +} + +/// 读取项目配置文件(package.json / Cargo.toml / AI文档 等) +/// 所有候选文件按「存在则读,不存在跳过」策略处理,对简单项目零影响。 +fn read_config_files(root: &Path) -> String { + let mut parts = Vec::new(); + + let candidates: &[(&str, &str, usize)] = &[ + // 核心配置 + ("README.md", "README.md", 80), + ("package.json", "package.json", 60), + ("Cargo.toml", "Cargo.toml", 50), + ("go.mod", "go.mod", 30), + ("pyproject.toml", "pyproject.toml", 40), + ("requirements.txt", "requirements.txt", 30), + ("pom.xml", "pom.xml", 30), + // AI 上下文文档(存在才读;简单项目通常无此类文件) + ("ARCHITECTURE.md", "ARCHITECTURE.md", 120), + ("CLAUDE.md", "CLAUDE.md", 60), + ("AGENTS.md", "AGENTS.md", 60), + ("DEVELOPMENT.md", "DEVELOPMENT.md", 50), + ]; + + for (filename, label, max_lines) in candidates { + let content = read_file_short(root, filename, *max_lines); + if !content.is_empty() { + let ext = filename.rsplit('.').next().unwrap_or("txt"); + let lang = match ext { "json" => "json", "toml" => "toml", "xml" => "xml", _ => "" }; + // 用 ~~~ 围栏,避免文件内容含 ``` 时提前关闭代码块 + parts.push(format!("### {label}\n~~~{lang}\n{content}\n~~~")); + } + } + + if parts.is_empty() { "(无可读配置文件)".to_string() } else { parts.join("\n\n") } +} + +/// monorepo 子包 package.json 读取(最多 apps 前4个 + packages 前2个) +/// 仅在检测到 pnpm-workspace.yaml 时调用,对普通项目零影响。 +fn read_monorepo_subpackages(root: &Path) -> String { + let mut parts: Vec = Vec::new(); + + for (workspace_dir, limit) in &[("apps", 4usize), ("packages", 2)] { + let ws = root.join(workspace_dir); + if !ws.is_dir() { continue; } + let Ok(entries) = std::fs::read_dir(&ws) else { continue }; + + let mut dirs: Vec<_> = entries.flatten() + .filter(|e| e.path().is_dir()) + .collect(); + dirs.sort_by_key(|e| e.file_name()); + + for entry in dirs.iter().take(*limit) { + let content = read_file_short(&entry.path(), "package.json", 40); + if content.is_empty() { continue; } + let label = format!("{}/{}/package.json", workspace_dir, entry.file_name().to_string_lossy()); + parts.push(format!("### {label}\n~~~json\n{content}\n~~~")); + if parts.len() >= 6 { return parts.join("\n\n"); } + } + } + + parts.join("\n\n") +} + + fn scan_dir_tree(dir: &Path, depth: usize, max_depth: usize) -> String { const SKIP: &[&str] = &[ "node_modules", ".git", "target", "dist", ".next", ".nuxt", @@ -366,6 +572,8 @@ fn scan_dir_tree(dir: &Path, depth: usize, max_depth: usize) -> String { let name_str = name.to_string_lossy(); if name_str.starts_with('.') && name_str != ".blueprint" { continue; } if SKIP.iter().any(|s| *s == name_str.as_ref()) { continue; } + // 过滤 Vite 时间戳缓存文件(如 vite.config.ts.timestamp-xxx.mjs) + if name_str.contains(".timestamp-") { continue; } let path = entry.path(); if path.is_dir() { @@ -401,6 +609,31 @@ fn get_git_log(root: &Path) -> String { } } +/// init 用的 git 摘要:总提交数 + 最近 15 条 +fn get_git_summary(root: &Path) -> String { + let run = |args: &[&str]| { + std::process::Command::new("git") + .args(args) + .current_dir(root) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + }; + + let count = run(&["rev-list", "--count", "HEAD"]).unwrap_or_default(); + let log = run(&["log", "--oneline", "-15"]).unwrap_or_default(); + + if count.is_empty() && log.is_empty() { + return "(无 git 仓库或暂无提交)".to_string(); + } + + let mut parts = Vec::new(); + if !count.is_empty() { parts.push(format!("总提交数: {count}")); } + if !log.is_empty() { parts.push(format!("最近 15 条:\n{log}")); } + parts.join("\n") +} + fn read_blueprint_summary(root: &Path) -> String { let bp_dir = root.join(".blueprint"); if !bp_dir.exists() { return "(无现有蓝图)".to_string(); } @@ -436,18 +669,24 @@ fn build_init_prompt( project_name: &str, tech_stack: &str, dir_tree: &str, - readme: &str, - pkg_json: &str, - cargo_toml: &str, + config_files: &str, + git_summary: &str, + is_monorepo: bool, ) -> String { - let mut ctx = Vec::new(); - if !readme.is_empty() { ctx.push(format!("### README.md(前 60 行)\n```\n{readme}\n```")); } - if !pkg_json.is_empty() { ctx.push(format!("### package.json(前 40 行)\n```json\n{pkg_json}\n```")); } - if !cargo_toml.is_empty(){ ctx.push(format!("### Cargo.toml(前 40 行)\n```toml\n{cargo_toml}\n```")); } - let ctx_str = if ctx.is_empty() { "(无可读配置文件)".to_string() } else { ctx.join("\n\n") }; + let module_hint = if is_monorepo { + "建议 10-25 个(monorepo 项目请以**业务能力**为单位划分,不要以 app 目录为单位,否则粒度太粗、任务卡无法可派发)" + } else { + "建议 5-15 个" + }; + + let monorepo_note = if is_monorepo { + "\n> ⚠️ **monorepo 项目**:请以**业务能力**(如「用户认证」「某业务线数据管理」「数据层」)而非 app 目录为单位划分模块。每个 app 可能涉及多个业务模块,每个业务模块也可能跨 app 和 packages。\n" + } else { + "" + }; format!(r#"你是一个软件架构师。请基于以下项目信息,从零初始化这个项目的 .blueprint/ 蓝图。 - +{monorepo_note} ## 项目信息 - 名称: {project_name} - 技术栈: {tech_stack} @@ -458,15 +697,22 @@ fn build_init_prompt( ``` ## 项目配置文件 -{ctx_str} +{config_files} -## 蓝图规则 -项目根目录下已有 `.blueprint/CONVENTIONS.md`,请严格遵循其格式规范。 +## Git 提交摘要 +``` +{git_summary} +``` + +## 蓝图规则(CONVENTIONS.md 完整内容) +以下是本蓝图系统的格式规范,请严格遵循: + +{MASTER_CONVENTIONS} ## 你的任务 -1. 识别项目的主要功能模块(建议 5-15 个),合理划分 area(前端/后端/基础设施等) +1. 识别项目的主要功能模块({module_hint}),合理划分 area(前端/后端/基础设施等) 2. 创建 `.blueprint/manifest.yaml`(含 areas、modules、edges) -3. 为每个模块创建 `.blueprint/modules/.md`(含概述 + 任务卡) +3. 为每个模块创建 `.blueprint/modules/.md`(含概述 + 任务卡,每张任务卡必须包含 files、acceptance、complexity 字段) 4. 已实现的功能标记 ✅ done,未实现的标记 📋 todo 或 💭 concept 5. 完成后更新 manifest.yaml 的 updated 日期为今天 @@ -478,11 +724,52 @@ fn build_sync_prompt( project_name: &str, tech_stack: &str, dir_tree: &str, + config_files: &str, git_log: &str, blueprint_summary: &str, ) -> String { - format!(r#"你是一个软件架构师。请根据项目最新状态,同步更新这个项目的 .blueprint/ 蓝图,使其准确反映代码现实。 + // 检测关键输入是否缺失,生成上下文感知的提示段落 + let no_blueprint = blueprint_summary.contains("无现有蓝图") || blueprint_summary.trim().is_empty(); + let no_git = git_log.contains("无法读取") || git_log.trim().is_empty(); + let context_note = match (no_blueprint, no_git) { + (true, true) => "⚠️ 当前项目**尚无蓝图**且**无法读取 git 历史**。请根据目录结构和配置文件从零初始化蓝图,识别主要功能模块(建议 5-15 个),合理划分 area,为每个模块创建 modules/.md。", + (true, false) => "⚠️ 当前项目**尚无蓝图**。请根据目录结构、配置文件和 git 历史从零初始化蓝图,识别主要功能模块(建议 5-15 个),合理划分 area,已有代码的功能标记 ✅ done。", + (false, true) => "⚠️ **无法读取 git 历史**,跳过「对照提交标记完成」步骤,仅根据目录结构与现有蓝图的差异补充缺失模块和任务卡。", + (false, false) => "", + }; + + let tasks = if no_blueprint { + // 无蓝图:退化为 init 风格的任务列表 + "1. 识别项目的主要功能模块(建议 5-15 个),合理划分 area(前端/后端/基础设施等)\n\ + 2. 创建 `.blueprint/manifest.yaml`(含 areas、modules、edges)\n\ + 3. 为每个模块创建 `.blueprint/modules/.md`(含概述 + 任务卡,每张任务卡必须包含 files、acceptance、complexity 字段)\n\ + 4. 已有代码的功能标记 ✅ done,规划中的标记 📋 todo 或 💭 concept\n\ + 5. 更新 manifest.yaml 的 updated 日期为今天" + } else if no_git { + // 有蓝图但无 git:只做结构对比,不做提交归因 + "1. (跳过 git 对照,无法读取提交历史)\n\ + 2. 发现目录中存在但蓝图缺失的功能模块 → 补充对应模块和任务卡\n\ + 3. 将目录中已消失的功能标记为 abandoned 或删除对应模块\n\ + 4. 更新模块的 status 和 progress 使其与任务卡状态一致\n\ + 5. 更新 manifest.yaml 的 updated 日期为今天" + } else { + // 正常 sync:有蓝图 + 有 git + "1. 对照 git 提交记录,找出已完成但蓝图未标记的功能 → 更新对应任务卡为 ✅ done\n\ + 2. 发现代码/目录中存在但蓝图缺失的功能模块 → 补充对应模块和任务卡\n\ + 3. 将不再存在的功能标记为 abandoned 或删除\n\ + 4. 更新模块的 status 和 progress 使其与任务卡状态一致\n\ + 5. 更新 manifest.yaml 的 updated 日期为今天" + }; + + let output_hint = if no_blueprint { + "请直接输出需要创建的文件内容。" + } else { + "请直接输出需要修改的文件内容(只输出有变化的文件)。" + }; + + format!(r#"你是一个软件架构师。请根据项目最新状态,同步更新这个项目的 .blueprint/ 蓝图,使其准确反映代码现实。 +{context_note_section} ## 项目信息 - 名称: {project_name} - 技术栈: {tech_stack} @@ -492,6 +779,9 @@ fn build_sync_prompt( {dir_tree} ``` +## 项目配置文件 +{config_files} + ## 近期 Git 提交(最近 20 条) ``` {git_log} @@ -500,18 +790,30 @@ fn build_sync_prompt( ## 现有蓝图内容 {blueprint_summary} -## 蓝图规则 -项目根目录下已有 `.blueprint/CONVENTIONS.md`,请严格遵循其格式规范。 +## 蓝图规则(CONVENTIONS.md 完整内容) +以下是本蓝图系统的格式规范,请严格遵循: + +{MASTER_CONVENTIONS} ## 你的任务 -1. 对照 git 提交记录,找出已完成但蓝图未标记的功能 → 更新对应任务卡为 ✅ done -2. 发现代码/目录中存在但蓝图缺失的功能模块 → 补充对应模块和任务卡 -3. 将不再存在的功能标记为 abandoned 或删除 -4. 更新模块的 status 和 progress 使其与任务卡状态一致 -5. 更新 manifest.yaml 的 updated 日期为今天 +{tasks} -请直接输出需要修改的文件内容(只输出有变化的文件)。 -"#) +{output_hint} +"#, + context_note_section = if context_note.is_empty() { + "\n".to_string() + } else { + format!("\n> {context_note}\n\n") + }, + project_name = project_name, + tech_stack = tech_stack, + dir_tree = dir_tree, + config_files = config_files, + git_log = git_log, + blueprint_summary = blueprint_summary, + tasks = tasks, + output_hint = output_hint, + ) } fn compute_stats(manifest: &BlueprintManifest) -> BlueprintStats { @@ -566,3 +868,789 @@ fn compute_stats(manifest: &BlueprintManifest) -> BlueprintStats { stats } + +// ── 蓝图治理 commands ──────────────────────────────────────────────────────── + +#[derive(Debug, Serialize)] +pub struct BlueprintStatus { + pub status: String, // none | rules_outdated | content_stale | synced + pub conventions_version: Option, + pub claude_md_managed: bool, + pub claude_md_version: Option, + pub manifest_updated: Option, + pub master_version: String, +} + +#[tauri::command] +pub fn get_blueprint_status(project_path: String) -> Result { + let root = Path::new(&project_path); + let bp_dir = root.join(".blueprint"); + + if !bp_dir.exists() { + return Ok(BlueprintStatus { + status: "none".to_string(), + conventions_version: None, + claude_md_managed: false, + claude_md_version: None, + manifest_updated: None, + master_version: MASTER_CONVENTIONS_VERSION.to_string(), + }); + } + + // 读取项目 CONVENTIONS.md 版本 + let conventions_version = read_frontmatter_version(&bp_dir.join("CONVENTIONS.md")); + + // 检测 CLAUDE.md MANAGED 区 + let claude_md_path = root.join("CLAUDE.md"); + let (claude_md_managed, claude_md_version) = if claude_md_path.exists() { + let content = std::fs::read_to_string(&claude_md_path).unwrap_or_default(); + if let Some(ver) = extract_managed_version(&content) { + (true, Some(ver)) + } else { + (false, None) + } + } else { + (false, None) + }; + + // 读取 manifest updated 日期 + let manifest_updated = read_manifest_updated(&bp_dir.join("manifest.yaml")); + + // 判断状态 + let rules_current = conventions_version.as_deref() == Some(MASTER_CONVENTIONS_VERSION) + && claude_md_managed + && claude_md_version.as_deref() == Some(MASTER_CONVENTIONS_VERSION); + + let status = if !rules_current { + "rules_outdated".to_string() + } else if is_content_stale(&manifest_updated) { + "content_stale".to_string() + } else { + "synced".to_string() + }; + + Ok(BlueprintStatus { + status, + conventions_version, + claude_md_managed, + claude_md_version, + manifest_updated, + master_version: MASTER_CONVENTIONS_VERSION.to_string(), + }) +} + +#[derive(Debug, Serialize)] +pub struct SyncResult { + pub success: bool, + pub conventions_written: bool, + pub claude_md_action: String, // created | appended | updated | skipped + pub message: String, +} + +#[tauri::command] +pub fn sync_blueprint_rules(project_path: String) -> Result { + let root = Path::new(&project_path); + let bp_dir = root.join(".blueprint"); + + // 确保 .blueprint/ 存在 + std::fs::create_dir_all(&bp_dir) + .map_err(|e| format!("创建 .blueprint/ 失败: {e}"))?; + + // 1. 写入 CONVENTIONS.md + std::fs::write(bp_dir.join("CONVENTIONS.md"), MASTER_CONVENTIONS) + .map_err(|e| format!("写入 CONVENTIONS.md 失败: {e}"))?; + + // 2. 处理 CLAUDE.md + let claude_path = root.join("CLAUDE.md"); + let claude_action = if !claude_path.exists() { + // 创建新文件 + let content = format!("# {}\n\n{CLAUDE_MANAGED_TEMPLATE}\n", + root.file_name().and_then(|n| n.to_str()).unwrap_or("Project")); + std::fs::write(&claude_path, content) + .map_err(|e| format!("创建 CLAUDE.md 失败: {e}"))?; + "created".to_string() + } else { + let content = std::fs::read_to_string(&claude_path) + .map_err(|e| format!("读取 CLAUDE.md 失败: {e}"))?; + + if content.contains(""; + + if let (Some(start), Some(end_pos)) = (content.find(start_marker), content.find(end_marker)) { + let end = end_pos + end_marker.len(); + format!("{}{}{}", &content[..start], new_section, &content[end..]) + } else { + format!("{}\n\n{new_section}\n", content.trim_end()) + } +} + +// ── 飞轮数据层:跨项目聚合 ─────────────────────────────────────────────────── + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FlywheelCohort { + pub conventions_version: String, + pub project_count: u32, + /// 可派发率:dispatched(可派发数) / total tasks(最新快照均值) + pub dispatchable_rate: f64, + /// 任务完成率:done / total tasks(最新快照均值,反映整体进度) + pub completion_rate: f64, + /// 飞轮持续率:30天内有快照的项目比例 + pub continuity_rate: f64, + /// 阻塞密度:blocked / total tasks(最新快照均值) + pub blocked_density: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BlockedReasonCount { + pub reason: String, + pub count: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StalledProject { + pub name: String, + /// UTC 日期字符串 "YYYY-MM-DD",未知时为 "unknown" + pub last_active: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct IterationJump { + pub project: String, + pub from_iteration: u32, + pub to_iteration: u32, + /// 跳变前最后一条快照的任务完成率(done/total) + pub rate_before: f64, + /// 跳变后第一条快照的任务完成率 + pub rate_after: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FlywheelStats { + pub cohorts: Vec, + pub top_blocked_reasons: Vec, + /// 飞轮停转项目(60天无快照) + pub stalled_projects: Vec, + /// AI文档休眠项目(90天无快照,推断 Claude 长期未读) + pub ai_doc_stale_projects: Vec, + /// 迭代跳变记录(iteration 递增,说明发生了主动重构) + pub iteration_jumps: Vec, + /// 停滞模块(in_progress 但无可推进任务,格式 "project_name/module_id") + pub stalled_modules: Vec, + pub total_projects_scanned: u32, + pub projects_with_data: u32, +} + +/// 内部:单项目最新快照的指标摘要 +struct ProjectSnap { + total_tasks: u64, + done_tasks: u64, + dispatched_tasks: u64, + blocked_tasks: u64, + has_recent_snapshot: bool, // 30天内 +} + +#[tauri::command] +pub fn get_flywheel_stats(project_paths: Vec) -> Result { + let now = chrono::Utc::now(); + let thirty_days_ago = now - chrono::Duration::days(30); + let sixty_days_ago = now - chrono::Duration::days(60); + let ninety_days_ago = now - chrono::Duration::days(90); + + let mut total_scanned = 0u32; + let mut projects_with_data = 0u32; + + // cohort_version → [ProjectSnap] + let mut cohort_data: std::collections::HashMap> = Default::default(); + // blocked_reason → 累计次数 + let mut reason_counts: std::collections::HashMap = Default::default(); + let mut stalled_projects: Vec = Vec::new(); + let mut ai_doc_stale_projects: Vec = Vec::new(); + let mut iteration_jumps: Vec = Vec::new(); + let mut stalled_modules: Vec = Vec::new(); + + for path_str in &project_paths { + total_scanned += 1; + let root = Path::new(path_str.as_str()); + let usage_path = root.join(".blueprint").join("usage.json"); + if !usage_path.exists() { continue; } + + let json_str = match std::fs::read_to_string(&usage_path) { + Ok(s) => s, + Err(_) => continue, + }; + let root_val: serde_json::Value = match serde_json::from_str(&json_str) { + Ok(v) => v, + Err(_) => continue, + }; + + let project_name = root_val["project_name"].as_str() + .map(|s| s.to_string()) + .unwrap_or_else(|| { + root.file_name().and_then(|n| n.to_str()).unwrap_or("unknown").to_string() + }); + + let snapshots = match root_val["snapshots"].as_array() { + Some(arr) if !arr.is_empty() => arr, + _ => continue, + }; + projects_with_data += 1; + + // 所有快照中收集 blocked_reasons + for snap in snapshots.iter() { + if let Some(reasons) = snap["blocked_reasons"].as_array() { + for r in reasons { + if let Some(s) = r.as_str() { + *reason_counts.entry(s.to_string()).or_insert(0) += 1; + } + } + } + } + + // 最新快照 + let latest = snapshots.last().unwrap(); + + let conventions_version = latest["conventions_version"] + .as_str().unwrap_or("unknown").to_string(); + + let latest_dt = latest["at"].as_str() + .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)); + + let last_active = latest_dt + .map(|dt| dt.format("%Y-%m-%d").to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // 停转检测(60天无快照) + if latest_dt.map_or(true, |dt| dt < sixty_days_ago) { + stalled_projects.push(StalledProject { + name: project_name.clone(), + last_active: last_active.clone(), + }); + } + + // AI文档休眠检测(90天无快照) + if latest_dt.map_or(true, |dt| dt < ninety_days_ago) { + ai_doc_stale_projects.push(StalledProject { + name: project_name.clone(), + last_active: last_active.clone(), + }); + } + + let has_recent = latest_dt.map_or(false, |dt| dt >= thirty_days_ago); + + let snap = ProjectSnap { + total_tasks: latest["tasks"]["total"].as_u64().unwrap_or(0), + done_tasks: latest["tasks"]["done"].as_u64().unwrap_or(0), + dispatched_tasks:latest["tasks"]["dispatched"].as_u64().unwrap_or(0), + blocked_tasks: latest["tasks"]["blocked"].as_u64().unwrap_or(0), + has_recent_snapshot: has_recent, + }; + cohort_data.entry(conventions_version).or_default().push(snap); + + // ── 迭代跳变检测 ────────────────────────────────────────────────────── + { + let mut prev_iter: Option = None; + let mut prev_rate: f64 = 0.0; + for s in snapshots.iter() { + let iter = s["iteration"].as_u64().unwrap_or(1) as u32; + let total = s["tasks"]["total"].as_u64().unwrap_or(0); + let done = s["tasks"]["done"].as_u64().unwrap_or(0); + let rate = if total > 0 { done as f64 / total as f64 } else { 0.0 }; + if let Some(pi) = prev_iter { + if iter > pi { + let round3 = |v: f64| (v * 1000.0).round() / 1000.0; + iteration_jumps.push(IterationJump { + project: project_name.clone(), + from_iteration: pi, + to_iteration: iter, + rate_before: round3(prev_rate), + rate_after: round3(rate), + }); + } + } + prev_iter = Some(iter); + prev_rate = rate; + } + } + + // ── 停滞模块收集(最新快照) ────────────────────────────────────────── + if let Some(stalled) = latest["modules"]["stalled"].as_array() { + for s in stalled { + if let Some(module_id) = s.as_str() { + stalled_modules.push(format!("{}/{}", project_name, module_id)); + } + } + } + } + + // 构建 cohorts + let round3 = |v: f64| (v * 1000.0).round() / 1000.0; + + let avg = |snaps: &[ProjectSnap], f: fn(&ProjectSnap) -> Option| -> f64 { + let vals: Vec = snaps.iter().filter_map(f).collect(); + if vals.is_empty() { 0.0 } else { vals.iter().sum::() / vals.len() as f64 } + }; + + let mut cohorts: Vec = cohort_data.into_iter().map(|(version, snaps)| { + let count = snaps.len() as u32; + + let dispatchable_rate = avg(&snaps, |s| { + if s.total_tasks > 0 { Some(s.dispatched_tasks as f64 / s.total_tasks as f64) } else { None } + }); + let completion_rate = avg(&snaps, |s| { + if s.total_tasks > 0 { Some(s.done_tasks as f64 / s.total_tasks as f64) } else { None } + }); + let continuity_rate = snaps.iter().filter(|s| s.has_recent_snapshot).count() as f64 / count as f64; + let blocked_density = avg(&snaps, |s| { + if s.total_tasks > 0 { Some(s.blocked_tasks as f64 / s.total_tasks as f64) } else { None } + }); + + FlywheelCohort { + conventions_version: version, + project_count: count, + dispatchable_rate: round3(dispatchable_rate), + completion_rate: round3(completion_rate), + continuity_rate: round3(continuity_rate), + blocked_density: round3(blocked_density), + } + }).collect(); + + // 按版本降序 + cohorts.sort_by(|a, b| b.conventions_version.cmp(&a.conventions_version)); + + // TOP10 blocked reasons + let mut sorted_reasons: Vec<_> = reason_counts.into_iter().collect(); + sorted_reasons.sort_by(|a, b| b.1.cmp(&a.1)); + let top_blocked_reasons = sorted_reasons.into_iter().take(10) + .map(|(reason, count)| BlockedReasonCount { reason, count }) + .collect(); + + Ok(FlywheelStats { + cohorts, + top_blocked_reasons, + stalled_projects, + ai_doc_stale_projects, + iteration_jumps, + stalled_modules, + total_projects_scanned: total_scanned, + projects_with_data, + }) +} + +// ── 梦核提示词生成 ──────────────────────────────────────────────────────────── + +#[tauri::command] +pub fn generate_muhe_prompt(stats: FlywheelStats) -> String { + // ── 队列对比表 ──────────────────────────────────────────────────────────── + let cohort_table = if stats.cohorts.is_empty() { + "(暂无队列数据)".to_string() + } else { + let header = "| CONVENTIONS 版本 | 项目数 | 可派发率 | 任务完成率 | 飞轮持续率 | 阻塞密度 |"; + let sep = "|------------------|--------|----------|------------|------------|----------|"; + let rows: Vec = stats.cohorts.iter().map(|c| { + format!( + "| {} | {} | {:.1}% | {:.1}% | {:.1}% | {:.1}% |", + c.conventions_version, + c.project_count, + c.dispatchable_rate * 100.0, + c.completion_rate * 100.0, + c.continuity_rate * 100.0, + c.blocked_density * 100.0, + ) + }).collect(); + format!("{header}\n{sep}\n{}", rows.join("\n")) + }; + + // ── TOP blocked_reasons ─────────────────────────────────────────────────── + let blocked_summary = if stats.top_blocked_reasons.is_empty() { + "(暂无阻塞记录)".to_string() + } else { + stats.top_blocked_reasons.iter() + .map(|r| format!("- {}(出现 {} 次)", r.reason, r.count)) + .collect::>() + .join("\n") + }; + + // ── 停转项目 ────────────────────────────────────────────────────────────── + let stalled_summary = if stats.stalled_projects.is_empty() { + "(无停转项目)".to_string() + } else { + stats.stalled_projects.iter() + .map(|p| format!("- {} (最后活跃: {})", p.name, p.last_active)) + .collect::>() + .join("\n") + }; + + // ── AI文档休眠项目 ──────────────────────────────────────────────────────── + let ai_stale_summary = if stats.ai_doc_stale_projects.is_empty() { + "(无休眠项目)".to_string() + } else { + stats.ai_doc_stale_projects.iter() + .map(|p| format!("- {} (最后活跃: {})", p.name, p.last_active)) + .collect::>() + .join("\n") + }; + + // ── 迭代跳变项目 ────────────────────────────────────────────────────────── + let iteration_summary = if stats.iteration_jumps.is_empty() { + "(暂无迭代跳变记录)".to_string() + } else { + stats.iteration_jumps.iter() + .map(|j| { + let flag = if j.rate_after < j.rate_before - 0.30 { " ⚠️ 完成率大幅下降" } else { "" }; + format!( + "- {} 迭代 {} → {}(完成率 {:.1}% → {:.1}%){}", + j.project, j.from_iteration, j.to_iteration, + j.rate_before * 100.0, j.rate_after * 100.0, + flag + ) + }) + .collect::>() + .join("\n") + }; + + // ── 停滞模块 ────────────────────────────────────────────────────────────── + let stalled_modules_summary = if stats.stalled_modules.is_empty() { + "(暂无停滞模块)".to_string() + } else { + stats.stalled_modules.iter() + .map(|m| format!("- {}", m)) + .collect::>() + .join("\n") + }; + + format!(r#"你是炼境(Dev Manager)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。 + +## 飞轮运转数据(来自炼境跨项目聚合) + +### 项目统计 +- 已扫描项目:{total} 个 +- 有蓝图数据:{with_data} 个 + +### CONVENTIONS 版本队列对比 +{cohort_table} + +### TOP 阻塞原因(跨项目累计) +{blocked_summary} + +### 飞轮停转项目(60天无快照) +{stalled_summary} + +### AI文档休眠项目(90天无快照,Claude 可能长期未介入) +{ai_stale_summary} + +### 迭代跳变项目(iteration 递增,发生了主动重构) +{iteration_summary} + +### 停滞模块(in_progress 但无可推进任务,格式:项目名/模块id) +{stalled_modules_summary} + +--- + +## 当前 CONVENTIONS.md(v{conventions_version})完整内容 + +{conventions} + +--- + +## 你的任务:记忆巩固 + +请完成以下两件事: + +### 【固化】哪些模式值得写进 CONVENTIONS? + +判断标准: +1. 某类阻塞在多个项目重复出现 → 在 CONVENTIONS 增加预防性规范 +2. 有队列数据显示某版本后可派发率/任务完成率明显提升 → 提炼有效实践 +3. 当前 CONVENTIONS 未覆盖但数据反映的真实问题 + +输出格式: +- 直接给出可追加到 CONVENTIONS.md 的新章节或条目内容 +- 每条改动说明:改了什么 + 为什么(对应哪个数据信号) + +### 【遗忘】哪些内容可以归档或清理? + +判断标准: +1. 飞轮停转项目(上方列出)是否有对应 AI 文档可以归档 +2. CONVENTIONS 中是否有已过时或与数据矛盾的规则 +3. 队列数据是否显示某版本规则效果差 → 对应规则可以删改 + +输出格式: +- 归档候选:项目名 + 建议归档的文件路径 +- CONVENTIONS 删改候选:具体条目 + 理由 + +--- + +请直接输出结构化分析结果,不要重复我的数据,聚焦在你的判断和建议上。 +"#, + total = stats.total_projects_scanned, + with_data = stats.projects_with_data, + cohort_table = cohort_table, + blocked_summary = blocked_summary, + stalled_summary = stalled_summary, + ai_stale_summary = ai_stale_summary, + iteration_summary = iteration_summary, + stalled_modules_summary = stalled_modules_summary, + conventions_version = MASTER_CONVENTIONS_VERSION, + conventions = MASTER_CONVENTIONS, + ) +} + +// ── 飞轮数据层:usage.json 快照 ────────────────────────────────────────────── + +/// 向项目 .blueprint/usage.json 追加一条每日飞轮快照。 +/// 节流:同一 UTC 日期只追加一次;写入失败静默忽略(不影响主流程)。 +fn append_usage_snapshot(bp_dir: &Path, manifest: &BlueprintManifest, stats: &BlueprintStats) { + let usage_path = bp_dir.join("usage.json"); + + // 项目名(bp_dir 是 /.blueprint/,父目录即项目根) + let project_name = bp_dir.parent() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + // 读取 CONVENTIONS 版本 + let conventions_version = read_frontmatter_version(&bp_dir.join("CONVENTIONS.md")) + .unwrap_or_else(|| "unknown".to_string()); + + // UTC 时间 + let now = chrono::Utc::now(); + let at = now.format("%Y-%m-%dT%H:%M:%SZ").to_string(); + let today = now.format("%Y-%m-%d").to_string(); + + // 读取或初始化 + let json_str = std::fs::read_to_string(&usage_path).unwrap_or_default(); + let mut root: serde_json::Value = if json_str.is_empty() { + serde_json::json!({ "project_name": project_name, "snapshots": [] }) + } else { + let mut v: serde_json::Value = serde_json::from_str(&json_str) + .unwrap_or_else(|_| serde_json::json!({ "snapshots": [] })); + // 补写 project_name(兼容旧版本没有此字段的文件) + if v["project_name"].is_null() { + v["project_name"] = serde_json::json!(project_name); + } + v + }; + + // 节流:今天已有快照则跳过 + if let Some(arr) = root["snapshots"].as_array() { + if arr.iter().any(|s| { + s["at"].as_str().map_or(false, |t| t.starts_with(&today)) + }) { + return; + } + } + + // 任务维度统计(BlueprintStats 未包含的字段) + let mut tasks_in_progress = 0u32; + let mut tasks_todo = 0u32; + let mut blocked_reasons: Vec = Vec::new(); + for module in &manifest.modules { + for task in &module.tasks { + match task.status.as_str() { + "in_progress" => tasks_in_progress += 1, + "todo" => tasks_todo += 1, + "blocked" => { + if let Some(ref reason) = task.blocked_reason { + let r = reason.trim().to_string(); + if !r.is_empty() { + blocked_reasons.push(r); + } + } + } + _ => {} + } + } + } + + // 停滞模块:in_progress 且任务全为 in_progress/blocked(没有 todo 可推进) + let stalled_modules: Vec = manifest.modules.iter() + .filter(|m| m.status == "in_progress" && !m.tasks.is_empty()) + .filter(|m| { + // 没有 todo 任务卡可以推进 + !m.tasks.iter().any(|t| t.status == "todo" && !t.locked) + }) + .map(|m| m.id.clone()) + .collect(); + + let snapshot = serde_json::json!({ + "at": at, + "conventions_version": conventions_version, + "iteration": manifest.iteration.unwrap_or(1), + "modules": { + "total": stats.total_modules, + "done": stats.done, + "in_progress": stats.in_progress, + "planned": stats.planned, + "concept": stats.concept, + "blocked": stats.blocked, + "stalled": stalled_modules // in_progress 但无可推进任务的模块 id 列表 + }, + "tasks": { + "total": stats.total_tasks, + "done": stats.tasks_done, + "in_progress": tasks_in_progress, + "blocked": stats.tasks_blocked, + "todo": tasks_todo, + "dispatched": stats.dispatchable + }, + "blocked_reasons": blocked_reasons + }); + + if let Some(arr) = root["snapshots"].as_array_mut() { + arr.push(snapshot); + } + + if let Ok(out) = serde_json::to_string_pretty(&root) { + let _ = std::fs::write(&usage_path, out); + } +} + +// ── 梦核:归档 AI 文档 ─────────────────────────────────────────────────────── + +#[derive(Debug, Serialize)] +pub struct ArchiveResult { + pub archived_files: Vec, + pub archive_dir: String, +} + +/// 将项目 .blueprint/modules/ 下所有 .md 文件归档到 .blueprint/archive// +/// 归档后写入 MANIFEST.txt,再删除原始文件。 +/// 不归档 CONVENTIONS.md / usage.json / manifest.yaml(这些是运行时文件)。 +#[tauri::command] +pub fn archive_project_docs(project_path: String) -> Result { + let root = Path::new(&project_path); + let bp_dir = root.join(".blueprint"); + let modules_dir = bp_dir.join("modules"); + + if !modules_dir.exists() { + return Err("项目不存在 .blueprint/modules/ 目录".to_string()); + } + + // 收集待归档文件 + let entries = std::fs::read_dir(&modules_dir) + .map_err(|e| format!("读取 modules/ 失败: {e}"))?; + + let mut files_to_archive: Vec = entries + .flatten() + .filter(|e| { + let name = e.file_name(); + let s = name.to_string_lossy(); + e.path().is_file() && s.ends_with(".md") + }) + .map(|e| e.path()) + .collect(); + + if files_to_archive.is_empty() { + return Err("modules/ 目录下没有可归档的 .md 文件".to_string()); + } + + files_to_archive.sort(); + + // 创建归档目录 + let timestamp = chrono::Utc::now().format("%Y-%m-%d-%H%M%S").to_string(); + let archive_dir = bp_dir.join("archive").join(×tamp); + std::fs::create_dir_all(&archive_dir) + .map_err(|e| format!("创建归档目录失败: {e}"))?; + + let mut archived: Vec = Vec::new(); + + // 复制文件 + for src in &files_to_archive { + let filename = src.file_name().unwrap(); + let dest = archive_dir.join(filename); + std::fs::copy(src, &dest) + .map_err(|e| format!("复制 {} 失败: {e}", src.display()))?; + archived.push(filename.to_string_lossy().to_string()); + } + + // 写 MANIFEST.txt + let manifest_content = format!( + "归档时间: {}\n项目: {}\n归档文件:\n{}\n", + timestamp, + root.file_name().and_then(|n| n.to_str()).unwrap_or("unknown"), + archived.iter().map(|f| format!(" - {f}")).collect::>().join("\n") + ); + std::fs::write(archive_dir.join("MANIFEST.txt"), manifest_content) + .map_err(|e| format!("写入 MANIFEST.txt 失败: {e}"))?; + + // 删除原始文件 + for src in &files_to_archive { + std::fs::remove_file(src) + .map_err(|e| format!("删除 {} 失败: {e}", src.display()))?; + } + + Ok(ArchiveResult { + archived_files: archived, + archive_dir: archive_dir.to_string_lossy().to_string(), + }) +} diff --git a/src-tauri/src/commands/groups.rs b/src-tauri/src/commands/groups.rs index b55bafe..1d2ce21 100644 --- a/src-tauri/src/commands/groups.rs +++ b/src-tauri/src/commands/groups.rs @@ -1,4 +1,5 @@ use crate::db::pool; +use crate::mcp_inject; use crate::models::{ProductGroup, ProjectGroupMap}; use rusqlite::params; use serde::Deserialize; @@ -64,6 +65,19 @@ pub fn update_group(id: String, input: GroupInput) -> Result<(), String> { #[tauri::command] 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 = conn + .prepare("SELECT project_id FROM project_group_map WHERE group_id = ?1") + .map_err(|e| e.to_string())? + .query_map(params![id], |row| row.get::<_, String>(0)) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + for project_id in &member_ids { + if let Err(e) = mcp_inject::remove_project_mcp(project_id, &id) { + eprintln!("[MCP remove] 删除组时清除失败 project={} group={}: {}", project_id, id, e); + } + } conn.execute("DELETE FROM product_groups WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) @@ -102,12 +116,20 @@ pub fn add_group_member(project_id: String, group_id: String, role: Option Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; + // 先清除 MCP 配置(需要在 DB 记录删除前查询项目路径) + if let Err(e) = mcp_inject::remove_project_mcp(&project_id, &group_id) { + eprintln!("[MCP remove] 清除失败 project={} group={}: {}", project_id, group_id, e); + } conn.execute( "DELETE FROM project_group_map WHERE project_id = ?1 AND group_id = ?2", params![project_id, group_id], diff --git a/src-tauri/src/commands/projects.rs b/src-tauri/src/commands/projects.rs index a837a59..0022836 100644 --- a/src-tauri/src/commands/projects.rs +++ b/src-tauri/src/commands/projects.rs @@ -1,4 +1,5 @@ use crate::db::pool; +use crate::mcp_inject; use crate::models::{ActivityLog, Project}; use rusqlite::params; use serde::{Deserialize, Serialize}; @@ -276,6 +277,20 @@ pub fn delete_project(id: String, delete_local: Option) -> Result<(), Stri ) .unwrap_or((None, None, None)); + // 清理产品组 MCP 配置(在删除 project_group_map 记录之前) + let group_ids: Vec = conn + .prepare("SELECT group_id FROM project_group_map WHERE project_id = ?1") + .map_err(|e| e.to_string())? + .query_map(params![id], |row| row.get::<_, String>(0)) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + for group_id in &group_ids { + if let Err(e) = mcp_inject::remove_project_mcp(&id, group_id) { + eprintln!("[MCP remove] 删除项目时清除失败 project={} group={}: {}", id, group_id, e); + } + } + // 清理关联数据(旧 FK 指向 projects 表,需手动清理) for table in &["activity_log", "project_tools", "project_group_map", "project_env_tools", "deploy_commands"] { diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index d957734..0c7899e 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -12,6 +12,7 @@ const GLOBAL_KEYS: &[&str] = &[ "v2rayn_db_path", // v2rayN 数据库路径 "v2raya_username", // v2rayA 登录用户名 "v2raya_password", // v2rayA 登录密码 + "mcp_port", // MCP Server 监听端口(全局,与空间无关) ]; /// 获取 settings: @@ -60,7 +61,11 @@ pub fn get_settings(space_id: Option) -> Result, #[tauri::command] pub fn update_settings(settings: HashMap, space_id: Option) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; - for (k, v) in settings { + let mut mcp_port_changed = false; + for (k, v) in &settings { + if k == "mcp_port" { + mcp_port_changed = true; + } if GLOBAL_KEYS.contains(&k.as_str()) || space_id.is_none() { conn.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", @@ -75,6 +80,13 @@ pub fn update_settings(settings: HashMap, space_id: Option)?; + let quit_i = MenuItem::with_id(app, "quit", "退出炼境", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show_i, &quit_i])?; + + TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu) + .show_menu_on_left_click(false) + .tooltip("炼境") + .on_menu_event(|app, event| match event.id.as_ref() { + "quit" => app.exit(0), + "show" => { + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + _ => {} + }) + .on_tray_icon_event(|tray, event| { + // 左键单击托盘图标 → 显示/聚焦窗口 + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let app = tray.app_handle(); + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + }) + .build(app)?; + + // ── 关闭窗口 → 最小化到托盘 ────────────────────────────── + if let Some(window) = app.get_webview_window("main") { + let win = window.clone(); + window.on_window_event(move |event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = win.hide(); + } + }); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -150,6 +219,12 @@ pub fn run() { generate_workflow, // blueprint get_blueprint, + generate_blueprint_prompt, + get_blueprint_status, + sync_blueprint_rules, + get_flywheel_stats, + generate_muhe_prompt, + archive_project_docs, // canvas state get_canvas_state, save_canvas_state, diff --git a/src-tauri/src/mcp_inject.rs b/src-tauri/src/mcp_inject.rs new file mode 100644 index 0000000..869a619 --- /dev/null +++ b/src-tauri/src/mcp_inject.rs @@ -0,0 +1,142 @@ +use crate::db; +use serde_json::{json, Value}; +use std::path::PathBuf; + +const MCP_KEY_PREFIX: &str = "lian-jing:group-"; + +// ── 路径解析 ────────────────────────────────────────────────────────────────── + +/// 解析项目根目录路径(WSL 项目自动转为 Windows UNC 路径) +fn project_root(project_id: &str) -> Result { + let conn = db::pool().get().map_err(|e| e.to_string())?; + + let row: rusqlite::Result<(Option, Option, Option)> = conn.query_row( + "SELECT win_path, wsl_path, platform FROM project_workspaces WHERE id = ?1", + [project_id], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ); + + let (win_path, wsl_path, platform) = + row.map_err(|_| format!("项目 {} 不存在", project_id))?; + + if platform.as_deref() == Some("wsl") { + let wsl = wsl_path.ok_or_else(|| "WSL 项目未配置 wsl_path".to_string())?; + let distro: String = conn + .query_row( + "SELECT value FROM settings WHERE key = 'wsl_distro'", + [], + |r| r.get(0), + ) + .unwrap_or_else(|_| "Ubuntu".to_string()); + let inner = wsl.trim_start_matches('/').replace('/', "\\"); + Ok(PathBuf::from(format!( + "\\\\wsl.localhost\\{}\\{}", + distro, inner + ))) + } else { + let win = win_path.ok_or_else(|| "Windows 项目未配置 win_path".to_string())?; + Ok(PathBuf::from(win)) + } +} + +fn settings_path(project_id: &str) -> Result { + Ok(project_root(project_id)?.join(".claude").join("settings.json")) +} + +// ── JSON 读写 ───────────────────────────────────────────────────────────────── + +fn read_json(path: &PathBuf) -> Result { + if !path.exists() { + return Ok(json!({})); + } + let content = std::fs::read_to_string(path) + .map_err(|e| format!("读取 settings.json 失败: {}", e))?; + if content.trim().is_empty() { + return Ok(json!({})); + } + serde_json::from_str(&content).map_err(|e| { + format!( + "settings.json 格式错误,请手动修复后重试({}): {}", + path.display(), + e + ) + }) +} + +fn write_json(path: &PathBuf, value: &Value) -> Result<(), String> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("创建 .claude 目录失败: {}", e))?; + } + let content = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?; + std::fs::write(path, content).map_err(|e| format!("写入 settings.json 失败: {}", e)) +} + +// ── 公开接口 ────────────────────────────────────────────────────────────────── + +/// 向项目的 .claude/settings.json 注入产品组 MCP 配置 +pub fn inject_project_mcp(project_id: &str, group_id: &str) -> Result<(), String> { + let port = crate::mcp_server::read_port(); + let path = settings_path(project_id)?; + let mut settings = read_json(&path)?; + + let servers = settings + .as_object_mut() + .ok_or("settings.json 顶层必须是 JSON 对象")? + .entry("mcpServers") + .or_insert(json!({})); + + let key = format!("{}{}", MCP_KEY_PREFIX, group_id); + servers[&key] = json!({ + "type": "http", + "url": format!("http://localhost:{}/mcp/group/{}", port, group_id) + }); + + write_json(&path, &settings) +} + +/// 从项目的 .claude/settings.json 移除产品组 MCP 配置 +pub fn remove_project_mcp(project_id: &str, group_id: &str) -> Result<(), String> { + let path = settings_path(project_id)?; + if !path.exists() { + return Ok(()); + } + let mut settings = read_json(&path)?; + + let key = format!("{}{}", MCP_KEY_PREFIX, group_id); + if let Some(servers) = settings + .get_mut("mcpServers") + .and_then(|v| v.as_object_mut()) + { + servers.remove(&key); + } + + write_json(&path, &settings) +} + +/// 端口变更后重新注入所有产品组成员的 MCP 配置 +/// 返回注入失败的错误列表(部分失败不影响其他项目) +pub fn reinject_all_mcp() -> Vec { + let conn = match db::pool().get() { + Ok(c) => c, + Err(e) => return vec![e.to_string()], + }; + + let mappings: Vec<(String, String)> = { + let mut stmt = match conn.prepare("SELECT project_id, group_id FROM project_group_map") { + Ok(s) => s, + Err(e) => return vec![e.to_string()], + }; + stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?))) + .map(|iter| iter.filter_map(|r| r.ok()).collect()) + .unwrap_or_default() + }; + + let mut errors = Vec::new(); + for (project_id, group_id) in mappings { + if let Err(e) = inject_project_mcp(&project_id, &group_id) { + errors.push(format!("项目 {}: {}", project_id, e)); + } + } + errors +} diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs new file mode 100644 index 0000000..bf389fe --- /dev/null +++ b/src-tauri/src/mcp_server.rs @@ -0,0 +1,289 @@ +use axum::{ + extract::Path, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::net::SocketAddr; + +use crate::db; + +/// 从 settings 表读取 MCP 端口,默认 27190 +pub fn read_port() -> u16 { + db::pool() + .get() + .ok() + .and_then(|c| { + c.query_row( + "SELECT value FROM settings WHERE key = 'mcp_port'", + [], + |r| r.get::<_, String>(0), + ) + .ok() + }) + .and_then(|v| v.parse().ok()) + .unwrap_or(27190) +} + +/// 启动 MCP HTTP 服务(Streamable HTTP transport) +pub async fn start(port: u16) { + let app = Router::new() + .route("/health", get(health)) + .route("/mcp/group/:group_id", post(handle_mcp)); + + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => { + println!("[MCP] 炼境 MCP Server 已启动,端口 {port}"); + if let Err(e) = axum::serve(listener, app).await { + eprintln!("[MCP] Server error: {e}"); + } + } + Err(e) => eprintln!("[MCP] 绑定端口 {port} 失败: {e}"), + } +} + +async fn health() -> &'static str { + "炼境 MCP Server running" +} + +// ── JSON-RPC 类型 ───────────────────────────────────────────────────────────── + +#[derive(Deserialize)] +struct RpcRequest { + #[allow(dead_code)] + jsonrpc: String, + method: String, + id: Option, + #[serde(default)] + params: Option, +} + +async fn handle_mcp(Path(group_id): Path, Json(req): Json) -> Response { + // MCP 通知(无 id)→ 202 Accepted,无需响应体 + if req.id.is_none() { + return StatusCode::ACCEPTED.into_response(); + } + + let id = req.id.clone(); + + let result = match req.method.as_str() { + "initialize" => json!({ + "protocolVersion": "2024-11-05", + "capabilities": { "tools": {} }, + "serverInfo": { + "name": "炼境", + "version": env!("CARGO_PKG_VERSION") + } + }), + "tools/list" => tools_list(), + "tools/call" => tools_call(&group_id, req.params.as_ref()).await, + _ => { + return Json(json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": -32601, "message": "Method not found" } + })) + .into_response(); + } + }; + + Json(json!({ "jsonrpc": "2.0", "id": id, "result": result })).into_response() +} + +// ── 工具定义 ────────────────────────────────────────────────────────────────── + +fn tools_list() -> Value { + json!({ + "tools": [ + { + "name": "list_group_projects", + "description": "列出产品组内所有成员项目,包含名称、路径、平台、技术栈、角色等信息", + "inputSchema": { + "type": "object", + "properties": {}, + "required": [] + } + }, + { + "name": "get_project_blueprint", + "description": "读取产品组内某个项目的蓝图文件(.blueprint/manifest.yaml),了解该项目的模块与任务状态", + "inputSchema": { + "type": "object", + "properties": { + "project_id": { + "type": "string", + "description": "项目 workspace ID(来自 list_group_projects 的返回结果)" + } + }, + "required": ["project_id"] + } + }, + { + "name": "get_project_file", + "description": "读取产品组内某个项目的任意文件内容,如 openapi.yaml、README.md、src/types.ts 等", + "inputSchema": { + "type": "object", + "properties": { + "project_id": { + "type": "string", + "description": "项目 workspace ID" + }, + "relative_path": { + "type": "string", + "description": "相对于项目根目录的文件路径,如 openapi.yaml 或 src/api/routes.ts" + } + }, + "required": ["project_id", "relative_path"] + } + } + ] + }) +} + +// ── 工具调用 ────────────────────────────────────────────────────────────────── + +async fn tools_call(group_id: &str, params: Option<&Value>) -> Value { + let tool_name = params + .and_then(|p| p.get("name")) + .and_then(|n| n.as_str()) + .unwrap_or("") + .to_string(); + let args = params + .and_then(|p| p.get("arguments")) + .cloned() + .unwrap_or(json!({})); + let gid = group_id.to_string(); + + let outcome = tokio::task::spawn_blocking(move || match tool_name.as_str() { + "list_group_projects" => list_group_projects(&gid), + "get_project_blueprint" => { + let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or(""); + get_project_blueprint(&gid, pid) + } + "get_project_file" => { + let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or(""); + let rel = args + .get("relative_path") + .and_then(|v| v.as_str()) + .unwrap_or(""); + get_project_file(&gid, pid, rel) + } + _ => Err(format!("未知工具: {}", tool_name)), + }) + .await; + + match outcome { + Ok(Ok(text)) => json!({ "content": [{ "type": "text", "text": text }] }), + Ok(Err(e)) => json!({ "content": [{ "type": "text", "text": e }], "isError": true }), + Err(e) => json!({ "content": [{ "type": "text", "text": e.to_string() }], "isError": true }), + } +} + +// ── 工具实现(同步,由 spawn_blocking 包裹)────────────────────────────────── + +fn list_group_projects(group_id: &str) -> Result { + let conn = db::pool().get().map_err(|e| e.to_string())?; + let mut stmt = conn + .prepare( + "SELECT pw.id, pp.name, pw.platform, pw.win_path, pw.wsl_path, + pp.tech_stack, pgm.role + FROM project_group_map pgm + JOIN project_workspaces pw ON pw.id = pgm.project_id + JOIN project_profiles pp ON pp.id = pw.profile_id + WHERE pgm.group_id = ?1", + ) + .map_err(|e| e.to_string())?; + + let rows: Vec = stmt + .query_map([group_id], |row| { + let id: String = row.get(0)?; + let name: String = row.get(1)?; + let platform: Option = row.get(2)?; + let win_path: Option = row.get(3)?; + let wsl_path: Option = row.get(4)?; + let tech_stack: Option = row.get(5)?; + let role: Option = row.get(6)?; + Ok(format!( + "- **{}** (id: `{}`)\n 平台: {} | 技术栈: {}{}\n Windows 路径: {}\n WSL 路径: {}", + name, + id, + platform.as_deref().unwrap_or("unknown"), + tech_stack.as_deref().unwrap_or("未配置"), + role.map(|r| format!(" | 角色: {}", r)).unwrap_or_default(), + win_path.as_deref().unwrap_or("未配置"), + wsl_path.as_deref().unwrap_or("未配置"), + )) + }) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + + if rows.is_empty() { + return Ok("该产品组暂无成员项目。".to_string()); + } + Ok(format!( + "产品组成员(共 {} 个项目):\n\n{}", + rows.len(), + rows.join("\n\n") + )) +} + +fn get_project_blueprint(group_id: &str, project_id: &str) -> Result { + let root = resolve_project_root(group_id, project_id)?; + let path = root.join(".blueprint").join("manifest.yaml"); + std::fs::read_to_string(&path) + .map_err(|e| format!("读取蓝图失败({}): {}", path.display(), e)) +} + +fn get_project_file(group_id: &str, project_id: &str, relative_path: &str) -> Result { + if relative_path.contains("..") { + return Err("路径不允许包含 \"..\"".to_string()); + } + let root = resolve_project_root(group_id, project_id)?; + let path = root.join(relative_path); + std::fs::read_to_string(&path) + .map_err(|e| format!("读取文件失败({}): {}", path.display(), e)) +} + +/// 根据 group_id + project_id 解析项目根目录路径 +/// WSL 项目自动转换为 Windows UNC 路径(\\wsl.localhost\\...) +fn resolve_project_root(group_id: &str, project_id: &str) -> Result { + let conn = db::pool().get().map_err(|e| e.to_string())?; + + let row: rusqlite::Result<(Option, Option, Option)> = conn.query_row( + "SELECT pw.win_path, pw.wsl_path, pw.platform + FROM project_group_map pgm + JOIN project_workspaces pw ON pw.id = pgm.project_id + WHERE pgm.group_id = ?1 AND pgm.project_id = ?2", + [group_id, project_id], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ); + + let (win_path, wsl_path, platform) = row.map_err(|_| { + format!("项目 {} 不属于产品组 {},或项目不存在", project_id, group_id) + })?; + + if platform.as_deref() == Some("wsl") { + let wsl = wsl_path.ok_or_else(|| "WSL 项目未配置 wsl_path".to_string())?; + let distro: String = conn + .query_row( + "SELECT value FROM settings WHERE key = 'wsl_distro'", + [], + |r| r.get(0), + ) + .unwrap_or_else(|_| "Ubuntu".to_string()); + // /home/user/project → \\wsl.localhost\Ubuntu\home\user\project + let inner = wsl.trim_start_matches('/').replace('/', "\\"); + Ok(std::path::PathBuf::from(format!( + "\\\\wsl.localhost\\{}\\{}", + distro, inner + ))) + } else { + let win = win_path.ok_or_else(|| "Windows 项目未配置 win_path".to_string())?; + Ok(std::path::PathBuf::from(win)) + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0180a00..9db8886 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "Dev Manager", + "title": "炼境", "width": 1280, "height": 800, "minWidth": 900, diff --git a/src/App.tsx b/src/App.tsx index d1035b2..ad3357a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,16 +11,30 @@ import { Toast } from "./components/ui/Toast"; import LoginPage from "./components/auth/LoginPage"; import { githubGetAccount, githubLogout, type GitAccount } from "./lib/commands"; import { useSpacesSync } from "./hooks/useSpacesSync"; +import { useUIStore } from "./store/ui"; +import { check as checkUpdate } from "@tauri-apps/plugin-updater"; export default function App() { const [page, setPage] = useState("overview"); const [account, setAccount] = useState(undefined); const qc = useQueryClient(); + const showToast = useUIStore((s) => s.showToast); useEffect(() => { githubGetAccount().then(setAccount).catch(() => setAccount(null)); }, []); + // 启动时静默检查更新 + useEffect(() => { + checkUpdate() + .then((update) => { + if (update?.available) { + showToast(`🆕 发现新版本 v${update.version},前往「设置」页更新`); + } + }) + .catch(() => {}); // 静默失败,不打扰用户 + }, []); + const handleLogout = async () => { await githubLogout(); qc.clear(); diff --git a/src/components/dashboard/BlueprintModal.tsx b/src/components/dashboard/BlueprintModal.tsx index 1630bd4..1d8ab86 100644 --- a/src/components/dashboard/BlueprintModal.tsx +++ b/src/components/dashboard/BlueprintModal.tsx @@ -14,10 +14,22 @@ import { import "@xyflow/react/dist/style.css"; import { getBlueprint, + getBlueprintStatus, + syncBlueprintRules, + generateBlueprintPrompt, + getProjects, + getFlywheelStats, + generateMuhePrompt, + archiveProjectDocs, type BlueprintData, type BlueprintModule, type BlueprintTask, type BlueprintArea, + type BlueprintStatus, + type SyncResult, + type Project, + type FlywheelStats, + type StalledProject, } from "../../lib/commands"; interface Props { @@ -222,7 +234,7 @@ function pickHandles( // ── 主组件 ─────────────────────────────────────────────────────────────────── -type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | null; +type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | { type: "governance" } | { type: "flywheel" } | null; export function BlueprintModal({ projectName, projectPath, onClose }: Props) { const [sidePanel, setSidePanel] = useState(null); @@ -237,6 +249,12 @@ export function BlueprintModal({ projectName, projectPath, onClose }: Props) { queryFn: () => getBlueprint(projectPath), }); + const { data: bpStatus } = useQuery({ + queryKey: ["bp-status", projectPath], + queryFn: () => getBlueprintStatus(projectPath), + enabled: !!projectPath, + }); + const handleNodeClick = useCallback((mod: BlueprintModule) => { setSidePanel({ type: "module", module: mod }); setPinnedModuleId((prev) => (prev === mod.id ? null : mod.id)); @@ -363,14 +381,44 @@ export function BlueprintModal({ projectName, projectPath, onClose }: Props) { )}

- +
+ + + +
{/* 统计条 */} {stats && (
+ {blueprint?.manifest.iteration != null && blueprint.manifest.iteration > 1 && ( + + 迭代 {blueprint.manifest.iteration} + + )} @@ -406,6 +454,30 @@ export function BlueprintModal({ projectName, projectPath, onClose }: Props) { 📋 {stats.dispatchable} 可派发 )} + {/* 规则 & AI文档 状态指示器 */} + {bpStatus && ( + + )} + {/* 总进度条(固定在右侧) */}
{pinnedModuleId && ( @@ -514,9 +586,19 @@ export function BlueprintModal({ projectName, projectPath, onClose }: Props) { onSelectModule={(mod) => { setSidePanel({ type: "module", module: mod }); setPinnedModuleId(mod.id); }} onClose={() => { setSidePanel(null); setPinnedModuleId(null); }} /> + ) : sidePanel.type === "governance" ? ( + setSidePanel(null)} + onOpenFlywheel={() => setSidePanel({ type: "flywheel" })} + /> + ) : sidePanel.type === "flywheel" ? ( + setSidePanel(null)} /> ) : ( { setSidePanel({ type: "module", module: mod }); setPinnedModuleId(mod.id); }} onClose={() => { setSidePanel(null); setPinnedModuleId(null); }} /> @@ -747,13 +829,40 @@ function TaskCard({ task, moduleName }: { task: BlueprintTask; moduleName?: stri function NextActionsPanel({ modules, + projectName, onSelectModule, onClose, }: { modules: BlueprintModule[]; + projectName: string; onSelectModule: (mod: BlueprintModule) => void; onClose: () => void; }) { + const [batchCopied, setBatchCopied] = useState(false); + + const handleCopyBatchPrompt = () => { + const prompt = [ + `当前项目:${projectName}`, + ``, + `读取 .blueprint/manifest.yaml 和所有 .blueprint/modules/*.md,`, + `以及 .blueprint/CONVENTIONS.md 了解执行规则。`, + ``, + `【处理断点】先检查是否有 🔵 in_progress 任务卡(上次中断的),`, + `若有,读取其 files 字段判断已完成什么、还缺什么,从断点继续。`, + ``, + `【批量执行】找出所有 📋 todo 任务卡,按任务卡 depends 字段及 manifest.yaml edges 综合拓扑排序后依次执行:`, + `- 执行前:前缀改为 🔵(信息1·加锁)`, + `- 执行依据:任务卡的 files 字段(涉及哪些文件)+ acceptance 字段(验收标准)`, + `- 完成后:前缀改为 ✅(信息2·解锁);若模块所有任务卡均完成,模块 status 改为 done`, + `- 更新 manifest.yaml 的 updated 日期`, + `- 无法独立解决:标记 🔴 blocked + blocked_reason,跳过继续下一张`, + ``, + `中途不需要等我确认,直接执行到底,全部完成后输出执行报告。`, + ].join("\n"); + navigator.clipboard.writeText(prompt); + setBatchCopied(true); + setTimeout(() => setBatchCopied(false), 2000); + }; // 收集所有可派发和进行中的任务 const actionItems = useMemo(() => { const items: { module: BlueprintModule; task: BlueprintTask; dispatchable: boolean }[] = []; @@ -784,7 +893,16 @@ function NextActionsPanel({

下一步

- +
+ + +
{actionItems.length === 0 ? ( @@ -877,6 +995,225 @@ function NextActionsPanel({ ); } +// ── 蓝图治理面板 ──────────────────────────────────────────────────────────── + +function GovernancePanel({ + projectPath, + projectName, + onClose, + onOpenFlywheel, +}: { + projectPath: string; + projectName: string; + onClose: () => void; + onOpenFlywheel?: () => void; +}) { + const { data: bpStatus, isPending: statusLoading, refetch: refetchStatus } = useQuery({ + queryKey: ["bp-status-governance", projectPath], + queryFn: () => getBlueprintStatus(projectPath), + }); + + const [syncing, setSyncing] = useState(false); + const [syncResult, setSyncResult] = useState(null); + const [syncError, setSyncError] = useState(null); + + const [promptMode, setPromptMode] = useState<"init" | "sync" | null>(null); + const [prompt, setPrompt] = useState(""); + const [promptLoading, setPromptLoading] = useState(false); + const [promptError, setPromptError] = useState(null); + const [copied, setCopied] = useState(false); + + const handleSyncRules = async () => { + setSyncing(true); + setSyncError(null); + setSyncResult(null); + try { + const res = await syncBlueprintRules(projectPath); + setSyncResult(res); + refetchStatus(); + } catch (e) { + setSyncError(String(e)); + } finally { + setSyncing(false); + } + }; + + const handleOpenPrompt = async (mode: "init" | "sync") => { + if (promptMode === mode) { + setPromptMode(null); + return; + } + setPromptMode(mode); + setPrompt(""); + setPromptError(null); + setPromptLoading(true); + try { + const result = await generateBlueprintPrompt(projectPath, mode); + setPrompt(result); + } catch (e) { + setPromptError(String(e)); + } finally { + setPromptLoading(false); + } + }; + + const handleCopy = () => { + navigator.clipboard.writeText(prompt).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + const rulesOutdated = bpStatus?.status === "rules_outdated"; + const currentVersion = bpStatus?.conventions_version ?? "—"; + const masterVersion = bpStatus?.master_version ?? "—"; + // 用后端完整状态判断(rules_outdated 才是真正未同步,content_stale/synced 都意味着规则已同步) + const versionSynced = bpStatus?.status === "synced" || bpStatus?.status === "content_stale"; + + return ( +
+ {/* 标题 */} +
+

蓝图治理

+ +
+ + {/* 规则版本状态 */} +
+

CONVENTIONS 规则版本

+ {statusLoading ? ( +

检测中…

+ ) : ( +
+
+ 当前版本 + {currentVersion} +
+
+ 最新版本 + {masterVersion} +
+
+ 状态 + {versionSynced ? ( + 🟢 已同步 + ) : ( + 🟡 待更新 + )} +
+
+ )} +
+ + {/* 同步规则区域 */} + {rulesOutdated && !syncResult && ( +
+

规则版本待更新

+

+ 将覆盖写入 .blueprint/CONVENTIONS.md 并更新 CLAUDE.md 中的蓝图规则区块。 +

+ {syncError && ( +

{syncError}

+ )} + +
+ )} + + {syncResult && ( +
+

✓ 同步完成

+

{syncResult.message}

+
+ )} + + {/* 蓝图内容提示词 */} +
+

蓝图内容

+

+ 将提示词交给 Claude Opus,由它负责理解和创作蓝图内容。 +

+
+ + +
+
+ + {/* 提示词内容区 */} + {promptMode && ( +
+ {promptLoading ? ( +

生成提示词中…

+ ) : promptError ? ( +

{promptError}

+ ) : ( + <> +