From 157df716c7f4657fe1d5c5e716be3644ee06e0d9 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Sat, 4 Apr 2026 23:20:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=81=A5=E5=BA=B7=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E4=BC=98=E5=8C=96=20+=20CONVENTIONS=20v1.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 健康中心: - check_editor 修正为读 editors/active_editors 新字段结构,兼容旧 active_editor - check_github_token 区分 401/403(Token 失效)与网络超时(warn 而非 error) 蓝图规则: - CONVENTIONS v1.4.0:新增「交互质量复盘笔记」章节,建立结构化复盘数据集 - blueprint.rs 版本常量同步至 1.4.0,CLAUDE.md 注入模板版本同步 - 项目导师模块标记为 done(progress 100) --- .blueprint/CONVENTIONS.md | 48 +++++++++++++++++- .blueprint/manifest.yaml | 4 +- .blueprint/modules/project-mentor.md | 42 ++++++++++++++++ .blueprint/usage.json | 14 +++--- CLAUDE.md | 2 +- src-tauri/src/commands/blueprint.rs | 4 +- src-tauri/src/commands/settings.rs | 75 +++++++++++++++++++++------- 7 files changed, 157 insertions(+), 32 deletions(-) diff --git a/.blueprint/CONVENTIONS.md b/.blueprint/CONVENTIONS.md index b7e2299..62a6174 100644 --- a/.blueprint/CONVENTIONS.md +++ b/.blueprint/CONVENTIONS.md @@ -1,6 +1,6 @@ --- -version: 1.3.0 -updated: 2026-04-03 +version: 1.4.0 +updated: 2026-04-04 source: dev-manager-tauri --- @@ -266,6 +266,50 @@ Claude 应关注这些信号来决定是否更新蓝图: - 执行模型遇到无法解决的问题 → 标记 🔴 blocked + blocked_reason - Opus 看到 blocked 任务 → 分析原因,拆/补/调后产出新任务卡 +## 交互质量复盘笔记 + +> 本章节仅适用于**开发执行阶段**(实现代码时),初始化蓝图或同步蓝图时无需执行。 + +每完成一张 **M 或 L 复杂度**的任务卡后,Claude 应通过 `append_project_note`(MCP 工具)写入一条结构化复盘笔记。这是飞轮外环的原始数据来源——积累足够多条后,Claude 读一遍即可识别高频坑点和高效模式。 + +### 笔记格式 + +``` +【复盘】<任务标题> +任务类型: 后端命令 | 前端组件 | 数据层 | MCP工具 | 架构调整 +复杂度: S | M | L +实际轮数: N轮(预估 M 轮) +是否返工: 是/否 +返工原因: <若返工,一句话说明根因,如"WSL路径格式未预料"> +有效策略: <本次奏效的做法,一句话> +遗留风险: <若有,一句话> +``` + +### 触发时机 + +| 情况 | 操作 | +|------|------| +| M/L 任务卡标记为 ✅ done | 写一条复盘笔记(source: mcp) | +| 遇到非预期的系统边界或平台差异 | 额外写一条"陷阱记录",遗留风险填具体复现条件 | +| 同类问题第二次出现 | 在新笔记中引用旧笔记时间戳,说明"同类问题复发" | + +### 示例 + +``` +【复盘】WSL项目路径检测 +任务类型: 后端命令 +复杂度: M +实际轮数: 6轮(预估 2 轮) +是否返工: 是 +返工原因: wsl_path 字段存的是 Windows UNC 格式(\\wsl.localhost\Ubuntu-22.04\...),而非 Linux 路径,导致 wsl 子命令收到错误参数 +有效策略: 在函数入口统一用 unc_to_distro_and_linux() 转换,一次解析,全程复用 +遗留风险: Path::exists() 对 \\wsl.localhost\ 路径无效,所有 WSL 路径存在性检查必须走 wsl -d distro -- test -d/f +``` + +> 这些笔记不替代决策记录(决策记录记架构 WHY,复盘笔记记执行 HOW 和踩坑)。两者都写。 + +--- + ## 飞轮外环(跨项目数据反哺) 内环描述的是单项目的执行流程。外环负责**把多个项目的执行数据聚合起来,反哺 CONVENTIONS 本身**。 diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index 2824bac..cdc1b7b 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -246,8 +246,8 @@ modules: - id: project-mentor name: 项目导师 area: frontend - status: in_progress - progress: 75 + status: done + progress: 100 position: [2000, 0] edges: diff --git a/.blueprint/modules/project-mentor.md b/.blueprint/modules/project-mentor.md index ee0ebd1..c2acbea 100644 --- a/.blueprint/modules/project-mentor.md +++ b/.blueprint/modules/project-mentor.md @@ -74,6 +74,48 @@ - depends: T1 - acceptance: 炼境自身(dev-manager-tauri)作为项目注册后,`get_mentor_context` 能正常返回其蓝图摘要和健康数据,不产生递归调用 +### ✅ T5 · project_notes 数据层 + 后端命令 +- status: done +- complexity: S +- files: src-tauri/src/db.rs, src-tauri/src/commands/mentor.rs, src-tauri/src/lib.rs +- depends: T1 +- acceptance: DB 增量迁移新增 `project_notes` 表;`get_project_notes(project_id)` 和 `add_project_note(project_id, content, source)` 两个 Tauri command 可通过 invoke 正常调用 + +### ✅ T6 · MCP append_project_note 工具 +- status: done +- complexity: S +- files: src-tauri/src/mcp_server.rs +- depends: T5 +- acceptance: MCP tools/list 新增 `append_project_note` 工具;Claude 通过 MCP 调用后,笔记写入 DB,返回成功确认;包含 group 归属校验(project_id 必须属于请求的 group) + +### ✅ T7 · MentorPanel 展示 AI 笔记 +- status: done +- complexity: S +- files: src/components/mentor/MentorPanel.tsx, src/lib/commands.ts +- depends: T5 +- acceptance: MentorPanel 底部新增"AI 笔记"区域,展示该项目最近 10 条笔记(时间 + 内容),无笔记时显示占位文案 + +### ✅ T8 · 后端全组巡检命令 +- status: done +- complexity: S +- files: src-tauri/src/commands/mentor.rs, src-tauri/src/lib.rs +- depends: T1 +- acceptance: `get_group_mentor_report(group_id)` 命令遍历组内所有项目,聚合每个项目的 mentor_markdown,返回一份完整的 Markdown 字符串,包含各项目小标题分隔 + +### ✅ T9 · 前端全组巡检按钮 +- status: done +- complexity: S +- files: src/components/manage/ManagePage.tsx, src/lib/commands.ts +- depends: T8 +- acceptance: ManagePage 产品组 Tab 的每个组卡片右上角出现"巡检全组"按钮;点击后加载中显示 spinner,完成后将聚合报告复制到剪贴板并提示"已复制,可粘贴给 Claude" + +### ✅ T10 · 启动健康扫描 + 前端 Alert Banner +- status: done +- complexity: M +- files: src-tauri/src/commands/mentor.rs, src-tauri/src/lib.rs, src/lib/commands.ts, src/App.tsx +- depends: T1 +- acceptance: 应用启动时后台扫描所有项目,超 14 天无 git 提交或蓝图存在 🔴 blocked 任务的项目写入告警(category=startup_alert);前端 App.tsx 加载完成后调用 `get_startup_alerts`,有结果则在页面顶部显示可关闭的黄色 Banner,列出需关注的项目名 + ## 决策记录 - 项目导师是洞察与引导层,health-center 是数据基础层,两者是上下游关系,不合并为一个模块 diff --git a/.blueprint/usage.json b/.blueprint/usage.json index 1ae631d..678ba18 100644 --- a/.blueprint/usage.json +++ b/.blueprint/usage.json @@ -30,20 +30,20 @@ "modules": { "blocked": 0, "concept": 0, - "done": 29, + "done": 32, "in_progress": 0, "planned": 0, "stalled": [], - "total": 29 + "total": 32 }, "tasks": { "blocked": 0, - "dispatched": 0, - "done": 106, + "dispatched": 1, + "done": 115, "in_progress": 0, - "todo": 0, - "total": 110 + "todo": 1, + "total": 116 } } ] -} \ No newline at end of file +} diff --git a/CLAUDE.md b/CLAUDE.md index 367f535..dc79f63 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 3f2bea6..4cceec8 100644 --- a/src-tauri/src/commands/blueprint.rs +++ b/src-tauri/src/commands/blueprint.rs @@ -4,10 +4,10 @@ use std::path::Path; // ── 主节点:master CONVENTIONS.md 内容和版本(编译时嵌入)──────────────────── const MASTER_CONVENTIONS: &str = include_str!("../../../.blueprint/CONVENTIONS.md"); -const MASTER_CONVENTIONS_VERSION: &str = "1.3.0"; +const MASTER_CONVENTIONS_VERSION: &str = "1.4.0"; // CLAUDE.md 蓝图管理区模板(版本与 CONVENTIONS 保持一致) -const CLAUDE_MANAGED_TEMPLATE: &str = r#" +const CLAUDE_MANAGED_TEMPLATE: &str = r#" ## 项目蓝图 本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index e6106a4..85a2599 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -340,7 +340,7 @@ fn check_github_token() -> HealthItem { _ => return Ok(skip_item("github_token", "GitHub Token", "未登录 GitHub 账户")), }; - match ureq::builder().timeout(std::time::Duration::from_secs(5)).build() + match ureq::builder().timeout(std::time::Duration::from_secs(8)).build() .get("https://api.github.com/user") .set("Authorization", &format!("Bearer {}", token)) .set("User-Agent", "dev-manager-tauri") @@ -353,42 +353,81 @@ fn check_github_token() -> HealthItem { Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "ok".into(), detail: Some(format!("Token 有效,账户: {}", username)) }) } - Err(e) => Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "error".into(), - detail: Some(format!("Token 失效或网络不可达: {}", e)) }), + Err(ureq::Error::Status(401, _)) | Err(ureq::Error::Status(403, _)) => + Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "error".into(), + detail: Some("Token 已失效或权限不足,请重新登录 GitHub 账户".into()) }), + Err(e) => Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "warn".into(), + detail: Some(format!("Token 已存储但无法联通 GitHub API(可能需要代理): {}", e)) }), } })(); result.unwrap_or_else(|e| err_item("github_token", "GitHub Token", &e)) } -/// 检测活跃编辑器路径是否可用(settings.active_editor JSON) +/// 检测活跃编辑器路径是否可用 +/// 存储结构:editors = [{id,name,path}],active_editors = ["id1","id2"] fn check_editor() -> HealthItem { let result: Result = (|| { let conn = pool().get().map_err(|e| e.to_string())?; - let raw: Option = conn.query_row( - "SELECT value FROM settings WHERE key = 'active_editor'", - [], |row| row.get(0), - ).ok(); - let raw = match raw { - Some(s) if !s.trim().is_empty() => s, - _ => return Ok(skip_item("editor", "编辑器路径", "未配置活跃编辑器")), + let get_val = |key: &str| -> Option { + conn.query_row( + "SELECT value FROM settings WHERE key = ?1", + rusqlite::params![key], |row| row.get(0), + ).ok() }; - let v: Value = serde_json::from_str(&raw) - .unwrap_or_else(|_| Value::Null); - let name = v["name"].as_str().unwrap_or("编辑器").to_string(); - let path = v["path"].as_str().unwrap_or("").to_string(); + // 读取编辑器列表 + let editors_raw = match get_val("editors") { + Some(s) if !s.trim().is_empty() => s, + _ => return Ok(skip_item("editor", "编辑器路径", "未配置任何编辑器")), + }; + let editors: Vec = serde_json::from_str(&editors_raw) + .unwrap_or_default(); + if editors.is_empty() { + return Ok(skip_item("editor", "编辑器路径", "未配置任何编辑器")); + } + + // 读取活跃编辑器 ID 列表(兼容旧单值字段) + let active_ids: Vec = if let Some(raw) = get_val("active_editors") { + serde_json::from_str(&raw).unwrap_or_default() + } else if let Some(old_id) = get_val("active_editor") { + if old_id.is_empty() { vec![] } else { vec![old_id] } + } else { + vec![] + }; + + if active_ids.is_empty() { + return Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "warn".into(), + detail: Some("已添加编辑器但未勾选活跃编辑器,请前往设置页勾选至少一个".into()) }); + } + + // 取第一个活跃编辑器检测路径 + let first_id = &active_ids[0]; + let entry = editors.iter().find(|e| e["id"].as_str() == Some(first_id)); + let (name, path) = match entry { + Some(e) => ( + e["name"].as_str().unwrap_or("编辑器").to_string(), + e["path"].as_str().unwrap_or("").to_string(), + ), + None => return Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "warn".into(), + detail: Some(format!("活跃编辑器 ID `{}` 在编辑器列表中未找到,请重新配置", first_id)) }), + }; if path.is_empty() { return Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "warn".into(), - detail: Some(format!("{} 未找到可执行路径", name)) }); + detail: Some(format!("{} 路径为空,请在设置页编辑该编辑器条目补全路径", name)) }); } if std::path::Path::new(&path).exists() { + let extra = if active_ids.len() > 1 { + format!("(共 {} 个活跃编辑器)", active_ids.len()) + } else { + String::new() + }; Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "ok".into(), - detail: Some(format!("{} 路径有效", name)) }) + detail: Some(format!("{} 路径有效{}", name, extra)) }) } else { Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "error".into(), - detail: Some(format!("{} 可执行文件不存在: {}", name, path)) }) + detail: Some(format!("{} 可执行文件不存在: {},请在设置页更新路径", name, path)) }) } })(); result.unwrap_or_else(|e| err_item("editor", "编辑器路径", &e))