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:
lanrtop 2026-04-08 03:27:04 +09:00
parent 4d9c003470
commit 56536effff
8 changed files with 383 additions and 29 deletions

View File

@ -6,7 +6,7 @@ description: >
成为每个项目的贴身导师。炼境本身亦被纳入其中, 成为每个项目的贴身导师。炼境本身亦被纳入其中,
形成可自我进化的开发智能闭环。 形成可自我进化的开发智能闭环。
iteration: 1 iteration: 1
updated: 2026-04-05 updated: 2026-04-08

View File

@ -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_handlerTS 封装完成
### ✅ 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 后来源恢复为内置

View File

@ -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

View File

@ -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/` 目录维护项目全景架构图,驱动需求讨论和任务管理。

View File

@ -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(),
) )
} }

View File

@ -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,

View File

@ -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">

View File

@ -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;