From 56536effff5679661aa0a70d1dcd7481c5a8be00 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Wed, 8 Apr 2026 03:27:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20P2=20CONVENTIONS=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=8C=96=E2=80=94=E2=80=94=E6=A2=A6=E6=A0=B8=E5=BB=BA=E8=AE=AE?= =?UTF-8?q?=E5=8F=AF=E7=9B=B4=E6=8E=A5=E5=86=99=E5=85=A5=E5=B9=B6=E5=88=86?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增用户版 CONVENTIONS 存储层(app_data_dir/conventions/CONVENTIONS.md) - OnceLock 初始化目录,effective_conventions_content() 用户版优先 fallback 内置 - invalidate_effective_cache() 写入/重置后清除缓存 - 新增三个 Tauri command - get_user_conventions_status:返回来源(builtin/user)、版本、内容 - save_user_conventions:解析 frontmatter version,写入用户版,自动同步所有项目 - reset_conventions_to_builtin:删除用户版,自动同步回内置版 - sync_blueprint_rules / build_init_prompt / build_sync_prompt / generate_muhe_prompt 全部切换为 effective_conventions_content(),规则更新无需发版 - GovernancePanel 新增规则编辑器 UI - 来源 badge(内置基线 / 用户自定义) - 「应用梦核建议」区域:粘贴新内容 + 确认写入 + 同步进度提示 - 「重置为基线」按钮(仅用户版存在时显示) --- .blueprint/manifest.yaml | 2 +- .blueprint/modules/blueprint-governance.md | 27 +++ .blueprint/modules/project-mentor.md | 4 +- CLAUDE.md | 2 +- src-tauri/src/commands/blueprint.rs | 213 +++++++++++++++++--- src-tauri/src/lib.rs | 6 +- src/components/dashboard/BlueprintModal.tsx | 136 +++++++++++++ src/lib/commands.ts | 22 ++ 8 files changed, 383 insertions(+), 29 deletions(-) diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index ef4582d..caa2c7e 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -6,7 +6,7 @@ description: > 成为每个项目的贴身导师。炼境本身亦被纳入其中, 形成可自我进化的开发智能闭环。 iteration: 1 -updated: 2026-04-05 +updated: 2026-04-08 diff --git a/.blueprint/modules/blueprint-governance.md b/.blueprint/modules/blueprint-governance.md index 53bf461..f8fcb5b 100644 --- a/.blueprint/modules/blueprint-governance.md +++ b/.blueprint/modules/blueprint-governance.md @@ -47,3 +47,30 @@ progress: 100 - complexity: S - files: src/components/settings/SettingsPage.tsx - acceptance: 设置页提供「同步所有项目蓝图规则」按钮,调用 sync_blueprint_rules 遍历所有已注册项目,返回每个项目的同步结果摘要 + +### ✅ 用户版 CONVENTIONS 存储层初始化 +- status: todo +- complexity: S +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs +- acceptance: 新增 `USER_CONVENTIONS_DIR: OnceLock` 和 `init_user_conventions_dir(app_data_dir)`,在 lib.rs setup() 中紧跟 db::init 调用;`effective_conventions_content() -> String` 优先读 app_data_dir/conventions/CONVENTIONS.md,文件不存在时 fallback 到编译时内置;`effective_conventions_version() -> String` 从 effective 内容解析 frontmatter version + +### ✅ 用户版 CONVENTIONS 读写 command +- status: todo +- complexity: M +- files: src-tauri/src/commands/blueprint.rs, src-tauri/src/lib.rs, src/lib/commands.ts +- depends: blueprint-governance +- acceptance: 新增三个 command:(1) `get_user_conventions_status` 返回 `{ source: "builtin"|"user", version, content, builtin_version, outdated_from_builtin }`;(2) `save_user_conventions(content)` 从 frontmatter 解析版本(失败时返回错误),写入用户版文件,清除 effective 内容缓存,自动触发遍历所有项目的 sync_blueprint_rules,返回 `{ version, synced_count }`;(3) `reset_conventions_to_builtin` 删除用户版文件并清除缓存;三个 command 注册到 invoke_handler,TS 封装完成 + +### ✅ sync/梦核/summary 切换为 effective 内容 +- status: todo +- complexity: S +- files: src-tauri/src/commands/blueprint.rs +- depends: blueprint-governance +- acceptance: `sync_blueprint_rules`、`generate_muhe_prompt`、`conventions_rules_summary`、`claude_managed_template` 全部改用 `effective_conventions_content()` 和 `effective_conventions_version()`,不再直接引用 MASTER_CONVENTIONS 常量;有用户版时分发用户版,无用户版时 fallback 内置版,行为与改前一致 + +### ✅ GovernancePanel 规则编辑器 UI +- status: todo +- complexity: M +- files: src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts +- depends: blueprint-governance +- acceptance: GovernancePanel 新增规则来源 badge(「内置基线 vX.X.X」或「用户自定义 vX.X.X」);新增「应用梦核建议」区域:文本框用于粘贴新 CONVENTIONS 内容 + 确认按钮,确认后调用 save_user_conventions,成功显示「已同步 N 个项目」;有用户版时显示「重置为基线」按钮,调用 reset_conventions_to_builtin 后来源恢复为内置 diff --git a/.blueprint/modules/project-mentor.md b/.blueprint/modules/project-mentor.md index c2acbea..5258b49 100644 --- a/.blueprint/modules/project-mentor.md +++ b/.blueprint/modules/project-mentor.md @@ -67,8 +67,8 @@ - depends: T1 - acceptance: 项目卡片底部出现"🎓"导师按钮,点击展示健康摘要和近期错误,"生成导师报告"按钮点击后将 mentor_markdown 复制到剪贴板 -### 📋 T4 · 炼境自指接入 -- status: todo +### 🔵 T4 · 炼境自指接入 +- status: in_progress - complexity: S - files: src-tauri/src/commands/mentor.rs - depends: T1 diff --git a/CLAUDE.md b/CLAUDE.md index dc79f63..91397b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ docs/ 部署和更新指南 - 代码重构/简化:/simplify → /code-review - 发现深层问题:/Dev-deep-think - + ## 项目蓝图 本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 diff --git a/src-tauri/src/commands/blueprint.rs b/src-tauri/src/commands/blueprint.rs index c1e088a..2aa89c5 100644 --- a/src-tauri/src/commands/blueprint.rs +++ b/src-tauri/src/commands/blueprint.rs @@ -5,22 +5,86 @@ use std::path::Path; const MASTER_CONVENTIONS: &str = include_str!("../../../.blueprint/CONVENTIONS.md"); -/// 从 MASTER_CONVENTIONS frontmatter 自动解析版本号,无需手动维护常量。 +/// 从 MASTER_CONVENTIONS frontmatter 自动解析版本号(内置基线版本,不随用户版变化)。 fn master_conventions_version() -> &'static str { static VERSION: std::sync::OnceLock = std::sync::OnceLock::new(); VERSION.get_or_init(|| { - for line in MASTER_CONVENTIONS.lines() { - if let Some(v) = line.strip_prefix("version: ") { - return v.trim().to_string(); - } - } - "unknown".to_string() + parse_frontmatter_version_str(MASTER_CONVENTIONS) + .unwrap_or_else(|| "unknown".to_string()) }) } -/// CLAUDE.md 蓝图管理区模板,版本号从 CONVENTIONS frontmatter 自动同步。 +// ── 用户版 CONVENTIONS(运行时可写,优先级高于内置)──────────────────────────── + +/// app data dir 下的 conventions/ 子目录路径,由 init_user_conventions_dir 初始化。 +static USER_CONVENTIONS_DIR: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// effective 内容缓存(None = 未缓存,Some = 已缓存用户版;内置版不缓存,直接用常量)。 +static USER_CONVENTIONS_CACHE: std::sync::Mutex> = std::sync::Mutex::new(None); + +/// 由 lib.rs setup() 调用,初始化用户版存储目录。 +pub fn init_user_conventions_dir(app_data_dir: std::path::PathBuf) { + let dir = app_data_dir.join("conventions"); + std::fs::create_dir_all(&dir).ok(); + USER_CONVENTIONS_DIR.set(dir).ok(); +} + +fn user_conventions_path() -> Option { + USER_CONVENTIONS_DIR.get().map(|d| d.join("CONVENTIONS.md")) +} + +/// 清除 effective 内容缓存,下次调用 effective_conventions_content() 时重新读文件。 +pub(crate) fn invalidate_effective_cache() { + if let Ok(mut cache) = USER_CONVENTIONS_CACHE.lock() { + *cache = None; + } +} + +/// 返回当前生效的 CONVENTIONS 内容:用户版(app data)优先,fallback 编译时内置。 +pub(crate) fn effective_conventions_content() -> String { + // 先查缓存 + if let Ok(cache) = USER_CONVENTIONS_CACHE.lock() { + if let Some(ref content) = *cache { + return content.clone(); + } + } + // 尝试读用户版文件 + if let Some(path) = user_conventions_path() { + if let Ok(content) = std::fs::read_to_string(&path) { + if !content.is_empty() { + if let Ok(mut cache) = USER_CONVENTIONS_CACHE.lock() { + *cache = Some(content.clone()); + } + return content; + } + } + } + // fallback 内置 + MASTER_CONVENTIONS.to_string() +} + +/// 返回当前生效的 CONVENTIONS 版本号。 +pub(crate) fn effective_conventions_version() -> String { + let content = effective_conventions_content(); + parse_frontmatter_version_str(&content) + .unwrap_or_else(|| master_conventions_version().to_string()) +} + +/// 从字符串内容解析 frontmatter version 字段。 +fn parse_frontmatter_version_str(content: &str) -> Option { + for line in content.lines() { + if let Some(v) = line.strip_prefix("version: ") { + return Some(v.trim().to_string()); + } + // frontmatter 结束标志(第二个 ---) + if line == "---" && !content.starts_with("---\n") { break; } + } + None +} + +/// CLAUDE.md 蓝图管理区模板,版本号使用当前生效的 CONVENTIONS 版本。 fn claude_managed_template() -> String { - let ver = master_conventions_version(); + let ver = effective_conventions_version(); format!(r#" ## 项目蓝图 @@ -722,7 +786,7 @@ fn build_init_prompt( ## 蓝图规则(CONVENTIONS.md 完整内容) 以下是本蓝图系统的格式规范,请严格遵循: -{MASTER_CONVENTIONS} +{conventions} ## 你的任务 1. 识别项目的主要功能模块({module_hint}),合理划分 area(前端/后端/基础设施等) @@ -732,7 +796,9 @@ fn build_init_prompt( 5. 完成后更新 manifest.yaml 的 updated 日期为今天 请直接输出需要创建的文件内容。 -"#) +"#, + conventions = effective_conventions_content(), + ) } fn build_sync_prompt( @@ -811,7 +877,7 @@ fn build_sync_prompt( ## 蓝图规则(CONVENTIONS.md 完整内容) 以下是本蓝图系统的格式规范,请严格遵循: -{MASTER_CONVENTIONS} +{conventions} ## 你的任务 {tasks} @@ -831,6 +897,7 @@ fn build_sync_prompt( blueprint_summary = blueprint_summary, tasks = tasks, output_hint = output_hint, + conventions = effective_conventions_content(), ) } @@ -974,8 +1041,8 @@ pub fn sync_blueprint_rules(project_path: String) -> Result 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) + // 1. 写入 CONVENTIONS.md(使用当前生效版本:用户版优先,fallback 内置) + std::fs::write(bp_dir.join("CONVENTIONS.md"), effective_conventions_content()) .map_err(|e| format!("写入 CONVENTIONS.md 失败: {e}"))?; // 2. 处理 CLAUDE.md @@ -1015,17 +1082,114 @@ pub fn sync_blueprint_rules(project_path: String) -> Result }) } +// ── 用户版 CONVENTIONS 读写 commands ───────────────────────────────────────── + +#[derive(Debug, Serialize)] +pub struct UserConventionsStatus { + pub source: String, // "builtin" | "user" + pub version: String, + pub content: String, + pub builtin_version: String, + pub outdated_from_builtin: bool, // 用户版本号低于内置时为 true +} + +#[tauri::command] +pub fn get_user_conventions_status() -> UserConventionsStatus { + let builtin_ver = master_conventions_version().to_string(); + let user_path = user_conventions_path(); + let has_user = user_path.as_ref() + .map(|p| p.exists()) + .unwrap_or(false); + + if has_user { + let content = effective_conventions_content(); + let version = effective_conventions_version(); + let outdated = parse_semver_tuple(&version) < parse_semver_tuple(&builtin_ver); + UserConventionsStatus { source: "user".to_string(), version, content, builtin_version: builtin_ver, outdated_from_builtin: outdated } + } else { + UserConventionsStatus { + source: "builtin".to_string(), + version: builtin_ver.clone(), + content: MASTER_CONVENTIONS.to_string(), + builtin_version: builtin_ver, + outdated_from_builtin: false, + } + } +} + +#[derive(Debug, Serialize)] +pub struct SaveConventionsResult { + pub version: String, + pub synced_count: u32, +} + +#[tauri::command] +pub fn save_user_conventions(content: String, project_paths: Vec) -> Result { + // 1. 解析版本号,非法内容拒绝写入 + let version = parse_frontmatter_version_str(&content) + .ok_or_else(|| "内容缺少有效的 frontmatter version 字段,写入已拒绝".to_string())?; + if version == "unknown" { + return Err("version 字段值为 unknown,写入已拒绝".to_string()); + } + + // 2. 写入用户版文件 + let path = user_conventions_path() + .ok_or_else(|| "用户版存储目录未初始化".to_string())?; + std::fs::write(&path, &content) + .map_err(|e| format!("写入用户版 CONVENTIONS 失败: {e}"))?; + + // 3. 清除缓存,使新版本立即生效 + invalidate_effective_cache(); + + // 4. 自动同步所有传入的项目 + let mut synced = 0u32; + for path_str in &project_paths { + if sync_blueprint_rules(path_str.clone()).is_ok() { + synced += 1; + } + } + + Ok(SaveConventionsResult { version, synced_count: synced }) +} + +#[tauri::command] +pub fn reset_conventions_to_builtin(project_paths: Vec) -> Result { + // 删除用户版文件 + if let Some(path) = user_conventions_path() { + if path.exists() { + std::fs::remove_file(&path) + .map_err(|e| format!("删除用户版文件失败: {e}"))?; + } + } + invalidate_effective_cache(); + + // 同步所有项目回内置版本 + let mut synced = 0u32; + for path_str in &project_paths { + if sync_blueprint_rules(path_str.clone()).is_ok() { + synced += 1; + } + } + + Ok(SaveConventionsResult { + version: master_conventions_version().to_string(), + synced_count: synced, + }) +} + +fn parse_semver_tuple(v: &str) -> (u32, u32, u32) { + let mut parts = v.splitn(3, '.'); + let major = parts.next().and_then(|x| x.parse().ok()).unwrap_or(0); + let minor = parts.next().and_then(|x| x.parse().ok()).unwrap_or(0); + let patch = parts.next().and_then(|x| x.parse().ok()).unwrap_or(0); + (major, minor, patch) +} + // ── 治理辅助函数 ───────────────────────────────────────────────────────────── fn read_frontmatter_version(path: &Path) -> Option { let content = std::fs::read_to_string(path).ok()?; - for line in content.lines() { - if let Some(v) = line.strip_prefix("version: ") { - return Some(v.trim().to_string()); - } - if line == "---" && !content.starts_with("---") { break; } - } - None + parse_frontmatter_version_str(&content) } fn extract_managed_version(content: &str) -> Option { @@ -1377,10 +1541,11 @@ fn conventions_rules_summary() -> String { "## 注意事项", ]; - let mut result = Vec::<&str>::new(); + let source = effective_conventions_content(); + let lines: Vec<&str> = source.lines().collect(); + let mut result: Vec<&str> = Vec::new(); let mut in_keep = false; let mut current_section_start = 0usize; - let lines: Vec<&str> = MASTER_CONVENTIONS.lines().collect(); for (i, &line) in lines.iter().enumerate() { let is_heading = line.starts_with("## ") || line.starts_with("### "); @@ -1557,7 +1722,7 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String { ai_stale_summary = ai_stale_summary, iteration_summary = iteration_summary, stalled_modules_summary = stalled_modules_summary, - conventions_version = master_conventions_version(), + conventions_version = effective_conventions_version(), conventions = conventions_rules_summary(), ) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d84013e..92e8443 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,7 +46,8 @@ pub fn run() { }; let data_dir = app.path().app_data_dir().expect("no app data dir"); - db::init(data_dir); + db::init(data_dir.clone()); + commands::blueprint::init_user_conventions_dir(data_dir); // 禁用 libgit2 目录所有者验证,允许访问 WSL UNC 路径(\\wsl.localhost\...) unsafe { let _ = git2::opts::set_verify_owner_validation(false); @@ -253,6 +254,9 @@ pub fn run() { get_flywheel_stats, generate_muhe_prompt, archive_project_docs, + get_user_conventions_status, + save_user_conventions, + reset_conventions_to_builtin, // canvas state get_canvas_state, save_canvas_state, diff --git a/src/components/dashboard/BlueprintModal.tsx b/src/components/dashboard/BlueprintModal.tsx index 44a137f..9eb6c1c 100644 --- a/src/components/dashboard/BlueprintModal.tsx +++ b/src/components/dashboard/BlueprintModal.tsx @@ -25,6 +25,9 @@ import { applyBlueprintBuff, removeBlueprintBuff, getBuffStatus, + getUserConventionsStatus, + saveUserConventions, + resetConventionsToBuiltin, type BlueprintData, type BlueprintModule, type BlueprintTask, @@ -35,6 +38,7 @@ import { type FlywheelStats, type StalledProject, type BuffStatus, + type UserConventionsStatus, } from "../../lib/commands"; interface Props { @@ -1071,6 +1075,17 @@ function GovernancePanel({ const [promptError, setPromptError] = useState(null); const [copied, setCopied] = useState(false); + // 用户版 CONVENTIONS 编辑器 + const { data: userConventions, refetch: refetchUserConventions } = useQuery({ + queryKey: ["user-conventions-status"], + queryFn: () => getUserConventionsStatus(), + }); + const [conventionsInput, setConventionsInput] = useState(""); + const [conventionsEditorOpen, setConventionsEditorOpen] = useState(false); + const [conventionsSaving, setConventionsSaving] = useState(false); + const [conventionsSaveResult, setConventionsSaveResult] = useState(null); + const [conventionsSaveError, setConventionsSaveError] = useState(null); + const handleApplyBuff = async () => { if (!projectId) return; setBuffLoading(true); @@ -1154,6 +1169,45 @@ function GovernancePanel({ }); }; + const handleSaveConventions = async () => { + if (!conventionsInput.trim()) return; + setConventionsSaving(true); + setConventionsSaveError(null); + setConventionsSaveResult(null); + try { + const projects = await getProjects(); + const paths = projects.map((p: Project) => p.win_path).filter((p): p is string => !!p); + const res = await saveUserConventions(conventionsInput, paths); + setConventionsSaveResult(`已应用 v${res.version},同步了 ${res.synced_count} 个项目`); + setConventionsInput(""); + setConventionsEditorOpen(false); + refetchUserConventions(); + refetchStatus(); + } catch (e) { + setConventionsSaveError(String(e)); + } finally { + setConventionsSaving(false); + } + }; + + const handleResetConventions = async () => { + setConventionsSaving(true); + setConventionsSaveError(null); + setConventionsSaveResult(null); + try { + const projects = await getProjects(); + const paths = projects.map((p: Project) => p.win_path).filter((p): p is string => !!p); + const res = await resetConventionsToBuiltin(paths); + setConventionsSaveResult(`已重置为内置基线 v${res.version},同步了 ${res.synced_count} 个项目`); + refetchUserConventions(); + refetchStatus(); + } catch (e) { + setConventionsSaveError(String(e)); + } finally { + setConventionsSaving(false); + } + }; + const rulesOutdated = bpStatus?.status === "rules_outdated"; const currentVersion = bpStatus?.conventions_version ?? "—"; const masterVersion = bpStatus?.master_version ?? "—"; @@ -1195,6 +1249,88 @@ function GovernancePanel({ )} + {/* 规则来源 badge */} + {userConventions && ( +
+ 规则来源 + {userConventions.source === "user" ? ( + + 用户自定义 v{userConventions.version} + {userConventions.outdated_from_builtin && ( + ⚠️ 低于内置 + )} + + ) : ( + + 内置基线 v{userConventions.builtin_version} + + )} +
+ )} + + {/* 应用梦核建议 */} +
+
+

应用梦核建议

+ +
+ {conventionsEditorOpen && ( +
+

+ 将梦核 Opus 输出的新 CONVENTIONS 内容粘贴到下方,确认后将写入并同步所有项目。 +

+