feat: P2 CONVENTIONS 动态化——梦核建议可直接写入并分发
- 新增用户版 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(内置基线 / 用户自定义) - 「应用梦核建议」区域:粘贴新内容 + 确认写入 + 同步进度提示 - 「重置为基线」按钮(仅用户版存在时显示)
This commit is contained in:
parent
4d9c003470
commit
56536effff
@ -6,7 +6,7 @@ description: >
|
|||||||
成为每个项目的贴身导师。炼境本身亦被纳入其中,
|
成为每个项目的贴身导师。炼境本身亦被纳入其中,
|
||||||
形成可自我进化的开发智能闭环。
|
形成可自我进化的开发智能闭环。
|
||||||
iteration: 1
|
iteration: 1
|
||||||
updated: 2026-04-05
|
updated: 2026-04-08
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -47,3 +47,30 @@ progress: 100
|
|||||||
- complexity: S
|
- complexity: S
|
||||||
- files: src/components/settings/SettingsPage.tsx
|
- files: src/components/settings/SettingsPage.tsx
|
||||||
- acceptance: 设置页提供「同步所有项目蓝图规则」按钮,调用 sync_blueprint_rules 遍历所有已注册项目,返回每个项目的同步结果摘要
|
- 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<PathBuf>` 和 `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 后来源恢复为内置
|
||||||
|
|||||||
@ -67,8 +67,8 @@
|
|||||||
- depends: T1
|
- depends: T1
|
||||||
- acceptance: 项目卡片底部出现"🎓"导师按钮,点击展示健康摘要和近期错误,"生成导师报告"按钮点击后将 mentor_markdown 复制到剪贴板
|
- acceptance: 项目卡片底部出现"🎓"导师按钮,点击展示健康摘要和近期错误,"生成导师报告"按钮点击后将 mentor_markdown 复制到剪贴板
|
||||||
|
|
||||||
### 📋 T4 · 炼境自指接入
|
### 🔵 T4 · 炼境自指接入
|
||||||
- status: todo
|
- status: in_progress
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- files: src-tauri/src/commands/mentor.rs
|
- files: src-tauri/src/commands/mentor.rs
|
||||||
- depends: T1
|
- depends: T1
|
||||||
|
|||||||
@ -36,7 +36,7 @@ docs/ 部署和更新指南
|
|||||||
- 代码重构/简化:/simplify → /code-review
|
- 代码重构/简化:/simplify → /code-review
|
||||||
- 发现深层问题:/Dev-deep-think
|
- 发现深层问题:/Dev-deep-think
|
||||||
|
|
||||||
<!-- BLUEPRINT_MANAGED_START version="1.4.0" -->
|
<!-- BLUEPRINT_MANAGED_START version="1.4.2" -->
|
||||||
## 项目蓝图
|
## 项目蓝图
|
||||||
|
|
||||||
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||||
|
|||||||
@ -5,22 +5,86 @@ use std::path::Path;
|
|||||||
|
|
||||||
const MASTER_CONVENTIONS: &str = include_str!("../../../.blueprint/CONVENTIONS.md");
|
const MASTER_CONVENTIONS: &str = include_str!("../../../.blueprint/CONVENTIONS.md");
|
||||||
|
|
||||||
/// 从 MASTER_CONVENTIONS frontmatter 自动解析版本号,无需手动维护常量。
|
/// 从 MASTER_CONVENTIONS frontmatter 自动解析版本号(内置基线版本,不随用户版变化)。
|
||||||
fn master_conventions_version() -> &'static str {
|
fn master_conventions_version() -> &'static str {
|
||||||
static VERSION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
static VERSION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||||
VERSION.get_or_init(|| {
|
VERSION.get_or_init(|| {
|
||||||
for line in MASTER_CONVENTIONS.lines() {
|
parse_frontmatter_version_str(MASTER_CONVENTIONS)
|
||||||
if let Some(v) = line.strip_prefix("version: ") {
|
.unwrap_or_else(|| "unknown".to_string())
|
||||||
return v.trim().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"unknown".to_string()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLAUDE.md 蓝图管理区模板,版本号从 CONVENTIONS frontmatter 自动同步。
|
// ── 用户版 CONVENTIONS(运行时可写,优先级高于内置)────────────────────────────
|
||||||
|
|
||||||
|
/// app data dir 下的 conventions/ 子目录路径,由 init_user_conventions_dir 初始化。
|
||||||
|
static USER_CONVENTIONS_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// effective 内容缓存(None = 未缓存,Some = 已缓存用户版;内置版不缓存,直接用常量)。
|
||||||
|
static USER_CONVENTIONS_CACHE: std::sync::Mutex<Option<String>> = 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<std::path::PathBuf> {
|
||||||
|
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<String> {
|
||||||
|
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 {
|
fn claude_managed_template() -> String {
|
||||||
let ver = master_conventions_version();
|
let ver = effective_conventions_version();
|
||||||
format!(r#"<!-- BLUEPRINT_MANAGED_START version="{ver}" -->
|
format!(r#"<!-- BLUEPRINT_MANAGED_START version="{ver}" -->
|
||||||
## 项目蓝图
|
## 项目蓝图
|
||||||
|
|
||||||
@ -722,7 +786,7 @@ fn build_init_prompt(
|
|||||||
## 蓝图规则(CONVENTIONS.md 完整内容)
|
## 蓝图规则(CONVENTIONS.md 完整内容)
|
||||||
以下是本蓝图系统的格式规范,请严格遵循:
|
以下是本蓝图系统的格式规范,请严格遵循:
|
||||||
|
|
||||||
{MASTER_CONVENTIONS}
|
{conventions}
|
||||||
|
|
||||||
## 你的任务
|
## 你的任务
|
||||||
1. 识别项目的主要功能模块({module_hint}),合理划分 area(前端/后端/基础设施等)
|
1. 识别项目的主要功能模块({module_hint}),合理划分 area(前端/后端/基础设施等)
|
||||||
@ -732,7 +796,9 @@ fn build_init_prompt(
|
|||||||
5. 完成后更新 manifest.yaml 的 updated 日期为今天
|
5. 完成后更新 manifest.yaml 的 updated 日期为今天
|
||||||
|
|
||||||
请直接输出需要创建的文件内容。
|
请直接输出需要创建的文件内容。
|
||||||
"#)
|
"#,
|
||||||
|
conventions = effective_conventions_content(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_sync_prompt(
|
fn build_sync_prompt(
|
||||||
@ -811,7 +877,7 @@ fn build_sync_prompt(
|
|||||||
## 蓝图规则(CONVENTIONS.md 完整内容)
|
## 蓝图规则(CONVENTIONS.md 完整内容)
|
||||||
以下是本蓝图系统的格式规范,请严格遵循:
|
以下是本蓝图系统的格式规范,请严格遵循:
|
||||||
|
|
||||||
{MASTER_CONVENTIONS}
|
{conventions}
|
||||||
|
|
||||||
## 你的任务
|
## 你的任务
|
||||||
{tasks}
|
{tasks}
|
||||||
@ -831,6 +897,7 @@ fn build_sync_prompt(
|
|||||||
blueprint_summary = blueprint_summary,
|
blueprint_summary = blueprint_summary,
|
||||||
tasks = tasks,
|
tasks = tasks,
|
||||||
output_hint = output_hint,
|
output_hint = output_hint,
|
||||||
|
conventions = effective_conventions_content(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -974,8 +1041,8 @@ pub fn sync_blueprint_rules(project_path: String) -> Result<SyncResult, String>
|
|||||||
std::fs::create_dir_all(&bp_dir)
|
std::fs::create_dir_all(&bp_dir)
|
||||||
.map_err(|e| format!("创建 .blueprint/ 失败: {e}"))?;
|
.map_err(|e| format!("创建 .blueprint/ 失败: {e}"))?;
|
||||||
|
|
||||||
// 1. 写入 CONVENTIONS.md
|
// 1. 写入 CONVENTIONS.md(使用当前生效版本:用户版优先,fallback 内置)
|
||||||
std::fs::write(bp_dir.join("CONVENTIONS.md"), MASTER_CONVENTIONS)
|
std::fs::write(bp_dir.join("CONVENTIONS.md"), effective_conventions_content())
|
||||||
.map_err(|e| format!("写入 CONVENTIONS.md 失败: {e}"))?;
|
.map_err(|e| format!("写入 CONVENTIONS.md 失败: {e}"))?;
|
||||||
|
|
||||||
// 2. 处理 CLAUDE.md
|
// 2. 处理 CLAUDE.md
|
||||||
@ -1015,17 +1082,114 @@ pub fn sync_blueprint_rules(project_path: String) -> Result<SyncResult, String>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 用户版 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<String>) -> Result<SaveConventionsResult, String> {
|
||||||
|
// 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<String>) -> Result<SaveConventionsResult, String> {
|
||||||
|
// 删除用户版文件
|
||||||
|
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<String> {
|
fn read_frontmatter_version(path: &Path) -> Option<String> {
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
for line in content.lines() {
|
parse_frontmatter_version_str(&content)
|
||||||
if let Some(v) = line.strip_prefix("version: ") {
|
|
||||||
return Some(v.trim().to_string());
|
|
||||||
}
|
|
||||||
if line == "---" && !content.starts_with("---") { break; }
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_managed_version(content: &str) -> Option<String> {
|
fn extract_managed_version(content: &str) -> Option<String> {
|
||||||
@ -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 in_keep = false;
|
||||||
let mut current_section_start = 0usize;
|
let mut current_section_start = 0usize;
|
||||||
let lines: Vec<&str> = MASTER_CONVENTIONS.lines().collect();
|
|
||||||
|
|
||||||
for (i, &line) in lines.iter().enumerate() {
|
for (i, &line) in lines.iter().enumerate() {
|
||||||
let is_heading = line.starts_with("## ") || line.starts_with("### ");
|
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,
|
ai_stale_summary = ai_stale_summary,
|
||||||
iteration_summary = iteration_summary,
|
iteration_summary = iteration_summary,
|
||||||
stalled_modules_summary = stalled_modules_summary,
|
stalled_modules_summary = stalled_modules_summary,
|
||||||
conventions_version = master_conventions_version(),
|
conventions_version = effective_conventions_version(),
|
||||||
conventions = conventions_rules_summary(),
|
conventions = conventions_rules_summary(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,8 @@ pub fn run() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let data_dir = app.path().app_data_dir().expect("no app data dir");
|
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\...)
|
// 禁用 libgit2 目录所有者验证,允许访问 WSL UNC 路径(\\wsl.localhost\...)
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = git2::opts::set_verify_owner_validation(false);
|
let _ = git2::opts::set_verify_owner_validation(false);
|
||||||
@ -253,6 +254,9 @@ pub fn run() {
|
|||||||
get_flywheel_stats,
|
get_flywheel_stats,
|
||||||
generate_muhe_prompt,
|
generate_muhe_prompt,
|
||||||
archive_project_docs,
|
archive_project_docs,
|
||||||
|
get_user_conventions_status,
|
||||||
|
save_user_conventions,
|
||||||
|
reset_conventions_to_builtin,
|
||||||
// canvas state
|
// canvas state
|
||||||
get_canvas_state,
|
get_canvas_state,
|
||||||
save_canvas_state,
|
save_canvas_state,
|
||||||
|
|||||||
@ -25,6 +25,9 @@ import {
|
|||||||
applyBlueprintBuff,
|
applyBlueprintBuff,
|
||||||
removeBlueprintBuff,
|
removeBlueprintBuff,
|
||||||
getBuffStatus,
|
getBuffStatus,
|
||||||
|
getUserConventionsStatus,
|
||||||
|
saveUserConventions,
|
||||||
|
resetConventionsToBuiltin,
|
||||||
type BlueprintData,
|
type BlueprintData,
|
||||||
type BlueprintModule,
|
type BlueprintModule,
|
||||||
type BlueprintTask,
|
type BlueprintTask,
|
||||||
@ -35,6 +38,7 @@ import {
|
|||||||
type FlywheelStats,
|
type FlywheelStats,
|
||||||
type StalledProject,
|
type StalledProject,
|
||||||
type BuffStatus,
|
type BuffStatus,
|
||||||
|
type UserConventionsStatus,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -1071,6 +1075,17 @@ function GovernancePanel({
|
|||||||
const [promptError, setPromptError] = useState<string | null>(null);
|
const [promptError, setPromptError] = useState<string | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
// 用户版 CONVENTIONS 编辑器
|
||||||
|
const { data: userConventions, refetch: refetchUserConventions } = useQuery<UserConventionsStatus>({
|
||||||
|
queryKey: ["user-conventions-status"],
|
||||||
|
queryFn: () => getUserConventionsStatus(),
|
||||||
|
});
|
||||||
|
const [conventionsInput, setConventionsInput] = useState("");
|
||||||
|
const [conventionsEditorOpen, setConventionsEditorOpen] = useState(false);
|
||||||
|
const [conventionsSaving, setConventionsSaving] = useState(false);
|
||||||
|
const [conventionsSaveResult, setConventionsSaveResult] = useState<string | null>(null);
|
||||||
|
const [conventionsSaveError, setConventionsSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleApplyBuff = async () => {
|
const handleApplyBuff = async () => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
setBuffLoading(true);
|
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 rulesOutdated = bpStatus?.status === "rules_outdated";
|
||||||
const currentVersion = bpStatus?.conventions_version ?? "—";
|
const currentVersion = bpStatus?.conventions_version ?? "—";
|
||||||
const masterVersion = bpStatus?.master_version ?? "—";
|
const masterVersion = bpStatus?.master_version ?? "—";
|
||||||
@ -1195,6 +1249,88 @@ function GovernancePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 规则来源 badge */}
|
||||||
|
{userConventions && (
|
||||||
|
<div className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
|
||||||
|
<span className="text-[11px] text-gray-500">规则来源</span>
|
||||||
|
{userConventions.source === "user" ? (
|
||||||
|
<span className="text-[11px] font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">
|
||||||
|
用户自定义 v{userConventions.version}
|
||||||
|
{userConventions.outdated_from_builtin && (
|
||||||
|
<span className="ml-1 text-yellow-600">⚠️ 低于内置</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[11px] font-mono text-gray-500 bg-white border border-gray-200 px-2 py-0.5 rounded">
|
||||||
|
内置基线 v{userConventions.builtin_version}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 应用梦核建议 */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">应用梦核建议</p>
|
||||||
|
<button
|
||||||
|
onClick={() => { setConventionsEditorOpen(v => !v); setConventionsSaveError(null); }}
|
||||||
|
className="text-[11px] text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{conventionsEditorOpen ? "收起" : "展开"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{conventionsEditorOpen && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[11px] text-gray-500 leading-relaxed">
|
||||||
|
将梦核 Opus 输出的新 CONVENTIONS 内容粘贴到下方,确认后将写入并同步所有项目。
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
value={conventionsInput}
|
||||||
|
onChange={e => setConventionsInput(e.target.value)}
|
||||||
|
placeholder="粘贴完整的 CONVENTIONS.md 内容(须包含 frontmatter version 字段)…"
|
||||||
|
rows={6}
|
||||||
|
className="w-full text-[11px] font-mono border border-gray-200 rounded p-2 resize-none focus:outline-none focus:border-blue-400"
|
||||||
|
/>
|
||||||
|
{conventionsSaveError && (
|
||||||
|
<p className="text-[11px] text-red-600 font-mono break-all">{conventionsSaveError}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSaveConventions}
|
||||||
|
disabled={conventionsSaving || !conventionsInput.trim()}
|
||||||
|
className="flex-1 py-1.5 rounded-lg bg-blue-500 text-white text-xs hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{conventionsSaving ? "写入中…" : "确认应用"}
|
||||||
|
</button>
|
||||||
|
{userConventions?.source === "user" && (
|
||||||
|
<button
|
||||||
|
onClick={handleResetConventions}
|
||||||
|
disabled={conventionsSaving}
|
||||||
|
className="px-3 py-1.5 rounded-lg border border-gray-200 text-gray-600 text-xs hover:bg-gray-50 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
重置为基线
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!conventionsEditorOpen && userConventions?.source === "user" && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[11px] text-gray-500">当前使用用户自定义规则</p>
|
||||||
|
<button
|
||||||
|
onClick={handleResetConventions}
|
||||||
|
disabled={conventionsSaving}
|
||||||
|
className="text-[11px] text-gray-400 hover:text-red-500 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
重置为基线
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{conventionsSaveResult && (
|
||||||
|
<p className="text-[11px] text-green-600 bg-green-50 rounded px-2 py-1">✓ {conventionsSaveResult}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 同步规则区域 */}
|
{/* 同步规则区域 */}
|
||||||
{rulesOutdated && !syncResult && (
|
{rulesOutdated && !syncResult && (
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 space-y-2">
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 space-y-2">
|
||||||
|
|||||||
@ -1001,6 +1001,28 @@ export const getBlueprintStatus = (projectPath: string) =>
|
|||||||
export const syncBlueprintRules = (projectPath: string) =>
|
export const syncBlueprintRules = (projectPath: string) =>
|
||||||
invoke<SyncResult>("sync_blueprint_rules", { projectPath });
|
invoke<SyncResult>("sync_blueprint_rules", { projectPath });
|
||||||
|
|
||||||
|
export interface UserConventionsStatus {
|
||||||
|
source: "builtin" | "user";
|
||||||
|
version: string;
|
||||||
|
content: string;
|
||||||
|
builtin_version: string;
|
||||||
|
outdated_from_builtin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveConventionsResult {
|
||||||
|
version: string;
|
||||||
|
synced_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserConventionsStatus = () =>
|
||||||
|
invoke<UserConventionsStatus>("get_user_conventions_status");
|
||||||
|
|
||||||
|
export const saveUserConventions = (content: string, projectPaths: string[]) =>
|
||||||
|
invoke<SaveConventionsResult>("save_user_conventions", { content, projectPaths });
|
||||||
|
|
||||||
|
export const resetConventionsToBuiltin = (projectPaths: string[]) =>
|
||||||
|
invoke<SaveConventionsResult>("reset_conventions_to_builtin", { projectPaths });
|
||||||
|
|
||||||
export interface FlywheelCohort {
|
export interface FlywheelCohort {
|
||||||
conventions_version: string;
|
conventions_version: string;
|
||||||
project_count: number;
|
project_count: number;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user