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
|
version: 1.4.0
|
||||||
updated: 2026-04-03
|
updated: 2026-04-04
|
||||||
source: dev-manager-tauri
|
source: dev-manager-tauri
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -266,6 +266,50 @@ Claude 应关注这些信号来决定是否更新蓝图:
|
|||||||
- 执行模型遇到无法解决的问题 → 标记 🔴 blocked + blocked_reason
|
- 执行模型遇到无法解决的问题 → 标记 🔴 blocked + blocked_reason
|
||||||
- Opus 看到 blocked 任务 → 分析原因,拆/补/调后产出新任务卡
|
- 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 本身**。
|
内环描述的是单项目的执行流程。外环负责**把多个项目的执行数据聚合起来,反哺 CONVENTIONS 本身**。
|
||||||
|
|||||||
@ -246,8 +246,8 @@ modules:
|
|||||||
- id: project-mentor
|
- id: project-mentor
|
||||||
name: 项目导师
|
name: 项目导师
|
||||||
area: frontend
|
area: frontend
|
||||||
status: in_progress
|
status: done
|
||||||
progress: 75
|
progress: 100
|
||||||
position: [2000, 0]
|
position: [2000, 0]
|
||||||
|
|
||||||
edges:
|
edges:
|
||||||
|
|||||||
@ -74,6 +74,48 @@
|
|||||||
- depends: T1
|
- depends: T1
|
||||||
- acceptance: 炼境自身(dev-manager-tauri)作为项目注册后,`get_mentor_context` 能正常返回其蓝图摘要和健康数据,不产生递归调用
|
- 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 是数据基础层,两者是上下游关系,不合并为一个模块
|
- 项目导师是洞察与引导层,health-center 是数据基础层,两者是上下游关系,不合并为一个模块
|
||||||
|
|||||||
@ -30,20 +30,20 @@
|
|||||||
"modules": {
|
"modules": {
|
||||||
"blocked": 0,
|
"blocked": 0,
|
||||||
"concept": 0,
|
"concept": 0,
|
||||||
"done": 29,
|
"done": 32,
|
||||||
"in_progress": 0,
|
"in_progress": 0,
|
||||||
"planned": 0,
|
"planned": 0,
|
||||||
"stalled": [],
|
"stalled": [],
|
||||||
"total": 29
|
"total": 32
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"blocked": 0,
|
"blocked": 0,
|
||||||
"dispatched": 0,
|
"dispatched": 1,
|
||||||
"done": 106,
|
"done": 115,
|
||||||
"in_progress": 0,
|
"in_progress": 0,
|
||||||
"todo": 0,
|
"todo": 1,
|
||||||
"total": 110
|
"total": 116
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ docs/ 部署和更新指南
|
|||||||
- 代码重构/简化:/simplify → /code-review
|
- 代码重构/简化:/simplify → /code-review
|
||||||
- 发现深层问题:/Dev-deep-think
|
- 发现深层问题:/Dev-deep-think
|
||||||
|
|
||||||
<!-- BLUEPRINT_MANAGED_START version="1.3.0" -->
|
<!-- BLUEPRINT_MANAGED_START version="1.4.0" -->
|
||||||
## 项目蓝图
|
## 项目蓝图
|
||||||
|
|
||||||
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||||
|
|||||||
@ -4,10 +4,10 @@ use std::path::Path;
|
|||||||
// ── 主节点:master CONVENTIONS.md 内容和版本(编译时嵌入)────────────────────
|
// ── 主节点:master CONVENTIONS.md 内容和版本(编译时嵌入)────────────────────
|
||||||
|
|
||||||
const MASTER_CONVENTIONS: &str = include_str!("../../../.blueprint/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 保持一致)
|
// 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/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||||
|
|||||||
@ -340,7 +340,7 @@ fn check_github_token() -> HealthItem {
|
|||||||
_ => return Ok(skip_item("github_token", "GitHub Token", "未登录 GitHub 账户")),
|
_ => 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")
|
.get("https://api.github.com/user")
|
||||||
.set("Authorization", &format!("Bearer {}", token))
|
.set("Authorization", &format!("Bearer {}", token))
|
||||||
.set("User-Agent", "dev-manager-tauri")
|
.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(),
|
Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "ok".into(),
|
||||||
detail: Some(format!("Token 有效,账户: {}", username)) })
|
detail: Some(format!("Token 有效,账户: {}", username)) })
|
||||||
}
|
}
|
||||||
Err(e) => Ok(HealthItem { id: "github_token".into(), name: "GitHub Token".into(), status: "error".into(),
|
Err(ureq::Error::Status(401, _)) | Err(ureq::Error::Status(403, _)) =>
|
||||||
detail: Some(format!("Token 失效或网络不可达: {}", e)) }),
|
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))
|
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 {
|
fn check_editor() -> HealthItem {
|
||||||
let result: Result<HealthItem, String> = (|| {
|
let result: Result<HealthItem, String> = (|| {
|
||||||
let conn = pool().get().map_err(|e| e.to_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 {
|
let get_val = |key: &str| -> Option<String> {
|
||||||
Some(s) if !s.trim().is_empty() => s,
|
conn.query_row(
|
||||||
_ => return Ok(skip_item("editor", "编辑器路径", "未配置活跃编辑器")),
|
"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 editors_raw = match get_val("editors") {
|
||||||
let name = v["name"].as_str().unwrap_or("编辑器").to_string();
|
Some(s) if !s.trim().is_empty() => s,
|
||||||
let path = v["path"].as_str().unwrap_or("").to_string();
|
_ => 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() {
|
if path.is_empty() {
|
||||||
return Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "warn".into(),
|
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() {
|
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(),
|
Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "ok".into(),
|
||||||
detail: Some(format!("{} 路径有效", name)) })
|
detail: Some(format!("{} 路径有效{}", name, extra)) })
|
||||||
} else {
|
} else {
|
||||||
Ok(HealthItem { id: "editor".into(), name: "编辑器路径".into(), status: "error".into(),
|
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))
|
result.unwrap_or_else(|e| err_item("editor", "编辑器路径", &e))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user