feat: 健康中心诊断优化 + CONVENTIONS v1.4.0
健康中心: - 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)
This commit is contained in:
parent
af88408669
commit
157df716c7
@ -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 本身**。
|
||||
|
||||
@ -246,8 +246,8 @@ modules:
|
||||
- id: project-mentor
|
||||
name: 项目导师
|
||||
area: frontend
|
||||
status: in_progress
|
||||
progress: 75
|
||||
status: done
|
||||
progress: 100
|
||||
position: [2000, 0]
|
||||
|
||||
edges:
|
||||
|
||||
@ -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 是数据基础层,两者是上下游关系,不合并为一个模块
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ docs/ 部署和更新指南
|
||||
- 代码重构/简化:/simplify → /code-review
|
||||
- 发现深层问题:/Dev-deep-think
|
||||
|
||||
<!-- BLUEPRINT_MANAGED_START version="1.3.0" -->
|
||||
<!-- BLUEPRINT_MANAGED_START version="1.4.0" -->
|
||||
## 项目蓝图
|
||||
|
||||
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||
|
||||
@ -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#"<!-- BLUEPRINT_MANAGED_START version="1.3.0" -->
|
||||
const CLAUDE_MANAGED_TEMPLATE: &str = r#"<!-- BLUEPRINT_MANAGED_START version="1.4.0" -->
|
||||
## 项目蓝图
|
||||
|
||||
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||
|
||||
@ -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<HealthItem, String> = (|| {
|
||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||
let raw: Option<String> = 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<String> {
|
||||
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<Value> = serde_json::from_str(&editors_raw)
|
||||
.unwrap_or_default();
|
||||
if editors.is_empty() {
|
||||
return Ok(skip_item("editor", "编辑器路径", "未配置任何编辑器"));
|
||||
}
|
||||
|
||||
// 读取活跃编辑器 ID 列表(兼容旧单值字段)
|
||||
let active_ids: Vec<String> = 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))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user