feat: 增强 Claude 项目理解能力——历史快照、近期提交、任务卡详情、AI 笔记
- db: 新增 project_snapshots 表,每日一条,UNIQUE(project_id, snapshot_date) - mentor: 启动扫描写入快照(days_since_commit / blueprint_done_rate / blocked / errors) - mentor: ProjectHealth 加入 recent_commits(最近 10 条含日期) - mentor: MentorContext 加入 active_tasks(进行中/阻塞任务卡含 files/acceptance) - mentor: build_mentor_markdown 加入 AI 笔记段落(跨会话记忆) - mcp: 新增 get_project_snapshots 工具,Claude 可查询历史趋势数据
This commit is contained in:
parent
f1cecd1601
commit
8d646c6cbb
@ -11,6 +11,8 @@ pub struct ProjectHealth {
|
||||
pub has_git: bool,
|
||||
/// git log --oneline -1 输出
|
||||
pub last_commit: Option<String>,
|
||||
/// 最近 10 条提交(hash + 日期 + message)
|
||||
pub recent_commits: Vec<String>,
|
||||
/// 项目记录的技术栈
|
||||
pub tech_stack: Option<String>,
|
||||
}
|
||||
@ -35,12 +37,23 @@ pub struct RuntimeLogEntry {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ActiveTaskCard {
|
||||
pub module_name: String,
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
pub files: Option<String>,
|
||||
pub acceptance: Option<String>,
|
||||
pub blocked_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MentorContext {
|
||||
pub project_id: String,
|
||||
pub project_name: String,
|
||||
pub health: ProjectHealth,
|
||||
pub blueprint_summary: Option<BlueprintSummary>,
|
||||
pub active_tasks: Vec<ActiveTaskCard>,
|
||||
pub recent_errors: Vec<RuntimeLogEntry>,
|
||||
/// 格式化好的 Markdown,可直接复制给 Claude
|
||||
pub mentor_markdown: String,
|
||||
@ -96,22 +109,24 @@ pub fn get_mentor_context(project_id: String) -> Result<MentorContext, String> {
|
||||
};
|
||||
|
||||
// 3. 本地健康检查
|
||||
let (path_valid, has_git, last_commit) = match (is_wsl, &wsl_linux_path, &wsl_distro) {
|
||||
let (path_valid, has_git, last_commit, recent_commits) = match (is_wsl, &wsl_linux_path, &wsl_distro) {
|
||||
(true, Some(linux_path), Some(distro)) => {
|
||||
let pv = wsl_test_dir(distro, linux_path);
|
||||
let hg = pv && wsl_test_dir(distro, &format!("{}/.git", linux_path));
|
||||
let lc = if hg { wsl_git_log(distro, linux_path) } else { None };
|
||||
(pv, hg, lc)
|
||||
let rc = if hg { wsl_git_log_recent(distro, linux_path) } else { vec![] };
|
||||
(pv, hg, lc, rc)
|
||||
}
|
||||
(true, _, _) => (false, false, None),
|
||||
(true, _, _) => (false, false, None, vec![]),
|
||||
(false, _, _) => match win_path.as_deref().filter(|p| !p.is_empty()).map(Path::new) {
|
||||
Some(root) => {
|
||||
let pv = root.exists();
|
||||
let hg = root.join(".git").exists();
|
||||
let lc = if hg { win_git_log(root) } else { None };
|
||||
(pv, hg, lc)
|
||||
let rc = if hg { win_git_log_recent(root) } else { vec![] };
|
||||
(pv, hg, lc, rc)
|
||||
}
|
||||
None => (false, false, None),
|
||||
None => (false, false, None, vec![]),
|
||||
},
|
||||
};
|
||||
|
||||
@ -119,6 +134,7 @@ pub fn get_mentor_context(project_id: String) -> Result<MentorContext, String> {
|
||||
path_valid,
|
||||
has_git,
|
||||
last_commit,
|
||||
recent_commits,
|
||||
tech_stack,
|
||||
};
|
||||
|
||||
@ -133,16 +149,35 @@ pub fn get_mentor_context(project_id: String) -> Result<MentorContext, String> {
|
||||
}
|
||||
};
|
||||
|
||||
// 4b. 进行中 / 阻塞任务卡详情
|
||||
let active_tasks = match (is_wsl, &wsl_linux_path, &wsl_distro) {
|
||||
(true, Some(linux_path), Some(distro)) => {
|
||||
let unc_root = linux_to_unc(linux_path, distro);
|
||||
read_active_task_cards(&unc_root)
|
||||
}
|
||||
(true, _, _) => vec![],
|
||||
(false, _, _) => {
|
||||
win_path.as_deref().filter(|p| !p.is_empty()).map(Path::new)
|
||||
.map(read_active_task_cards)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 近期错误日志
|
||||
let recent_errors = read_recent_errors(&conn, &project_id);
|
||||
|
||||
// 5b. AI 笔记(最近 5 条,作为跨会话记忆)
|
||||
let ai_notes = read_ai_notes(&conn, &project_id);
|
||||
|
||||
// 6. 生成 mentor_markdown
|
||||
let mentor_markdown = build_mentor_markdown(
|
||||
&project_id,
|
||||
&project_name,
|
||||
&health,
|
||||
&blueprint_summary,
|
||||
&active_tasks,
|
||||
&recent_errors,
|
||||
&ai_notes,
|
||||
);
|
||||
|
||||
Ok(MentorContext {
|
||||
@ -150,6 +185,7 @@ pub fn get_mentor_context(project_id: String) -> Result<MentorContext, String> {
|
||||
project_name,
|
||||
health,
|
||||
blueprint_summary,
|
||||
active_tasks,
|
||||
recent_errors,
|
||||
mentor_markdown,
|
||||
})
|
||||
@ -277,6 +313,41 @@ fn win_git_log(root: &Path) -> Option<String> {
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// 最近 10 条提交,格式:`abc1234 2026-04-08 fix: something`
|
||||
fn win_git_log_recent(root: &Path) -> Vec<String> {
|
||||
let output = crate::silent_cmd("git")
|
||||
.args(["log", "--pretty=format:%h %ad %s", "--date=short", "-10"])
|
||||
.current_dir(root)
|
||||
.output();
|
||||
match output {
|
||||
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| l.to_string())
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// WSL 版最近 10 条提交
|
||||
fn wsl_git_log_recent(distro: &str, linux_path: &str) -> Vec<String> {
|
||||
let output = crate::silent_cmd("wsl")
|
||||
.args([
|
||||
"-d", distro, "--", "git", "-C", linux_path,
|
||||
"log", "--pretty=format:%h %ad %s", "--date=short", "-10",
|
||||
])
|
||||
.stderr(std::process::Stdio::null())
|
||||
.output();
|
||||
match output {
|
||||
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| l.to_string())
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ── 蓝图摘要 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// WSL 项目:用 wsl 命令判断 manifest 存在性,再通过 UNC 路径读取内容
|
||||
@ -335,6 +406,31 @@ fn parse_blueprint_summary(root: &Path) -> Option<BlueprintSummary> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从项目根路径读取蓝图,返回所有进行中/阻塞的任务卡
|
||||
fn read_active_task_cards(root: &Path) -> Vec<ActiveTaskCard> {
|
||||
match crate::commands::blueprint::get_blueprint(root.to_string_lossy().to_string()) {
|
||||
Ok(Some(data)) => data
|
||||
.manifest
|
||||
.modules
|
||||
.into_iter()
|
||||
.flat_map(|m| {
|
||||
let module_name = m.name.clone();
|
||||
m.tasks.into_iter()
|
||||
.filter(|t| t.status == "in_progress" || t.status == "blocked")
|
||||
.map(move |t| ActiveTaskCard {
|
||||
module_name: module_name.clone(),
|
||||
title: t.title,
|
||||
status: t.status,
|
||||
files: t.files,
|
||||
acceptance: t.acceptance,
|
||||
blocked_reason: t.blocked_reason,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// ── 错误日志 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn read_recent_errors(
|
||||
@ -362,6 +458,29 @@ fn read_recent_errors(
|
||||
result.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_ai_notes(conn: &rusqlite::Connection, project_id: &str) -> Vec<ProjectNote> {
|
||||
let mut stmt = match conn.prepare(
|
||||
"SELECT id, project_id, source, content, created_at
|
||||
FROM project_notes
|
||||
WHERE project_id = ?1
|
||||
ORDER BY id DESC LIMIT 5",
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
stmt.query_map(params![project_id], |row| {
|
||||
Ok(ProjectNote {
|
||||
id: row.get(0)?,
|
||||
project_id: row.get(1)?,
|
||||
source: row.get(2)?,
|
||||
content: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
})
|
||||
})
|
||||
.map(|iter| iter.filter_map(|r| r.ok()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ── Markdown 生成 ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn build_mentor_markdown(
|
||||
@ -369,7 +488,9 @@ fn build_mentor_markdown(
|
||||
project_name: &str,
|
||||
health: &ProjectHealth,
|
||||
blueprint: &Option<BlueprintSummary>,
|
||||
active_tasks: &[ActiveTaskCard],
|
||||
errors: &[RuntimeLogEntry],
|
||||
ai_notes: &[ProjectNote],
|
||||
) -> String {
|
||||
let mut md = String::new();
|
||||
|
||||
@ -389,6 +510,16 @@ fn build_mentor_markdown(
|
||||
md.push_str(&format!("- 最近提交:`{}`\n", commit));
|
||||
}
|
||||
|
||||
// ── 近期提交历史 ──────────────────────────────────────────────────────────
|
||||
md.push_str("\n## 近期提交(最近 10 条)\n\n");
|
||||
if health.recent_commits.is_empty() {
|
||||
md.push_str("暂无提交记录。\n");
|
||||
} else {
|
||||
for c in &health.recent_commits {
|
||||
md.push_str(&format!("- `{}`\n", c));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 蓝图状态 ──────────────────────────────────────────────────────────────
|
||||
md.push_str("\n## 蓝图状态\n\n");
|
||||
if let Some(ref bp) = blueprint {
|
||||
@ -409,6 +540,30 @@ fn build_mentor_markdown(
|
||||
md.push_str("该项目尚未接入蓝图(无 .blueprint/manifest.yaml)。\n");
|
||||
}
|
||||
|
||||
// ── 进行中 / 阻塞任务卡 ───────────────────────────────────────────────────
|
||||
if !active_tasks.is_empty() {
|
||||
let in_progress: Vec<_> = active_tasks.iter().filter(|t| t.status == "in_progress").collect();
|
||||
let blocked: Vec<_> = active_tasks.iter().filter(|t| t.status == "blocked").collect();
|
||||
|
||||
if !in_progress.is_empty() {
|
||||
md.push_str("\n## 进行中任务\n\n");
|
||||
for t in &in_progress {
|
||||
md.push_str(&format!("### [{}] {}\n", t.module_name, t.title));
|
||||
if let Some(ref f) = t.files { md.push_str(&format!("- files: `{}`\n", f)); }
|
||||
if let Some(ref a) = t.acceptance { md.push_str(&format!("- acceptance: {}\n", a)); }
|
||||
}
|
||||
}
|
||||
|
||||
if !blocked.is_empty() {
|
||||
md.push_str("\n## 阻塞任务\n\n");
|
||||
for t in &blocked {
|
||||
md.push_str(&format!("### [{}] {}\n", t.module_name, t.title));
|
||||
if let Some(ref r) = t.blocked_reason { md.push_str(&format!("- blocked_reason: {}\n", r)); }
|
||||
if let Some(ref f) = t.files { md.push_str(&format!("- files: `{}`\n", f)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 近期错误日志 ──────────────────────────────────────────────────────────
|
||||
md.push_str("\n## 近期错误日志(最多 20 条)\n\n");
|
||||
if errors.is_empty() {
|
||||
@ -419,7 +574,15 @@ fn build_mentor_markdown(
|
||||
}
|
||||
}
|
||||
|
||||
md.push_str("\n---\n\n请根据以上信息,对该项目的当前状态、质量和下一步方向给出你的判断与建议。\n");
|
||||
// ── AI 笔记(跨会话记忆)────────────────────────────────────────────────────
|
||||
if !ai_notes.is_empty() {
|
||||
md.push_str("\n## AI 笔记(历史观察)\n\n");
|
||||
for note in ai_notes {
|
||||
md.push_str(&format!("- `{}` {}\n", note.created_at, note.content));
|
||||
}
|
||||
}
|
||||
|
||||
md.push_str("\n---\n\n请根据以上信息,对该项目的当前状态、质量和下一步方向给出你的判断与建议。如需历史趋势数据,可调用 `get_project_snapshots` 工具。\n");
|
||||
|
||||
md
|
||||
}
|
||||
@ -525,7 +688,8 @@ pub fn run_startup_health_scan() {
|
||||
for (project_id, project_name, win_path, wsl_path, platform) in rows {
|
||||
// 1. 检查 git 活跃度(超 14 天无提交)
|
||||
// 直接获取距今天数,None 表示路径不存在或无 git(跳过,不是"停滞")
|
||||
if let Some(days) = get_days_since_last_commit(platform.as_deref(), win_path.as_deref(), wsl_path.as_deref(), &conn) {
|
||||
let days_opt = get_days_since_last_commit(platform.as_deref(), win_path.as_deref(), wsl_path.as_deref(), &conn);
|
||||
if let Some(days) = days_opt {
|
||||
if days > 14 {
|
||||
// \x1f (ASCII Unit Separator) 作分隔符,避免项目名含 | 导致解析错位
|
||||
crate::db::log_event(
|
||||
@ -561,8 +725,25 @@ pub fn run_startup_health_scan() {
|
||||
win_path.as_deref().map(|p| std::path::Path::new(p).join(".blueprint"))
|
||||
};
|
||||
|
||||
if let Some(bp_dir) = blueprint_path_opt {
|
||||
let blocked_count = count_blocked_tasks(&bp_dir);
|
||||
let blocked_count = blueprint_path_opt
|
||||
.as_ref()
|
||||
.map(|bp_dir| count_blocked_tasks(bp_dir))
|
||||
.unwrap_or(0);
|
||||
|
||||
let (bp_done_rate, bp_in_progress) = blueprint_path_opt
|
||||
.as_ref()
|
||||
.and_then(|bp_dir| bp_dir.parent())
|
||||
.and_then(parse_blueprint_summary)
|
||||
.map(|s| {
|
||||
let rate = if s.module_count > 0 {
|
||||
Some(s.done_count as f64 / s.module_count as f64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(rate, s.in_progress)
|
||||
})
|
||||
.unwrap_or((None, 0));
|
||||
|
||||
if blocked_count > 0 {
|
||||
crate::db::log_event(
|
||||
"startup_alert",
|
||||
@ -571,7 +752,18 @@ pub fn run_startup_health_scan() {
|
||||
Some(&format!(r#"{{"project_id":"{}"}}"#, project_id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 写入每日快照(INSERT OR IGNORE 保证每天只存一条)
|
||||
let has_errors = has_recent_errors(&conn, &project_id);
|
||||
save_project_snapshot(
|
||||
&conn,
|
||||
&project_id,
|
||||
days_opt,
|
||||
bp_done_rate,
|
||||
bp_in_progress,
|
||||
blocked_count,
|
||||
has_errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -632,6 +824,45 @@ fn count_blocked_tasks(blueprint_dir: &std::path::Path) -> usize {
|
||||
count
|
||||
}
|
||||
|
||||
/// 检查该项目在最近 7 天内是否有 error 级别的日志
|
||||
fn has_recent_errors(conn: &rusqlite::Connection, project_id: &str) -> bool {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM runtime_logs
|
||||
WHERE level = 'error'
|
||||
AND (json_extract(context, '$.project_id') = ?1 OR context IS NULL)
|
||||
AND ts >= datetime('now', '-7 days')",
|
||||
params![project_id],
|
||||
|r| r.get::<_, i64>(0),
|
||||
)
|
||||
.unwrap_or(0) > 0
|
||||
}
|
||||
|
||||
/// 写入项目每日快照,INSERT OR IGNORE 保证每天只存一条
|
||||
fn save_project_snapshot(
|
||||
conn: &rusqlite::Connection,
|
||||
project_id: &str,
|
||||
days_since_commit: Option<u64>,
|
||||
blueprint_done_rate: Option<f64>,
|
||||
blueprint_in_progress: u32,
|
||||
blueprint_blocked: usize,
|
||||
has_errors: bool,
|
||||
) {
|
||||
let _ = conn.execute(
|
||||
"INSERT OR IGNORE INTO project_snapshots
|
||||
(project_id, snapshot_date, days_since_commit, blueprint_done_rate,
|
||||
blueprint_in_progress, blueprint_blocked, has_errors)
|
||||
VALUES (?1, date('now', 'localtime'), ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
project_id,
|
||||
days_since_commit.map(|d| d as i64),
|
||||
blueprint_done_rate,
|
||||
blueprint_in_progress,
|
||||
blueprint_blocked as i64,
|
||||
if has_errors { 1i64 } else { 0i64 },
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_startup_alerts() -> Result<Vec<StartupAlert>, String> {
|
||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||
|
||||
@ -514,6 +514,22 @@ fn migrate(conn: &rusqlite::Connection) {
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
").expect("create global_tools table");
|
||||
|
||||
// 增量迁移:项目历史快照(每日一条,供 Claude 做趋势分析)
|
||||
conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS project_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id TEXT NOT NULL,
|
||||
snapshot_date TEXT NOT NULL,
|
||||
days_since_commit INTEGER,
|
||||
blueprint_done_rate REAL,
|
||||
blueprint_in_progress INTEGER DEFAULT 0,
|
||||
blueprint_blocked INTEGER DEFAULT 0,
|
||||
has_errors INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(project_id, snapshot_date)
|
||||
);
|
||||
").expect("create project_snapshots table");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -180,6 +180,24 @@ fn tools_list_result() -> Value {
|
||||
},
|
||||
"required": ["project_id", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_project_snapshots",
|
||||
"description": "获取项目的历史健康快照(每日一条),包含 git 提交间隔、蓝图完成率、阻塞任务数、错误情况。用于分析趋势、识别高价值改进变量。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {
|
||||
"type": "string",
|
||||
"description": "项目 workspace ID(来自 list_group_projects 的返回结果)"
|
||||
},
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "查询最近 N 天的快照,默认 30"
|
||||
}
|
||||
},
|
||||
"required": ["project_id"]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
@ -223,6 +241,11 @@ async fn tools_call(group_id: &str, params: Option<&Value>) -> Value {
|
||||
let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
|
||||
append_project_note(&gid, pid, content)
|
||||
}
|
||||
"get_project_snapshots" => {
|
||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let days = args.get("days").and_then(|v| v.as_u64()).unwrap_or(30) as u32;
|
||||
get_project_snapshots(&gid, pid, days)
|
||||
}
|
||||
_ => Err(format!("未知工具: {}", tool_name)),
|
||||
})
|
||||
.await;
|
||||
@ -502,3 +525,74 @@ fn append_project_note(group_id: &str, project_id: &str, content: &str) -> Resul
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok("笔记已保存 ✅".to_string())
|
||||
}
|
||||
|
||||
fn get_project_snapshots(group_id: &str, project_id: &str, days: u32) -> Result<String, String> {
|
||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||
|
||||
// 校验项目属于该产品组
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM project_group_map WHERE group_id = ?1 AND project_id = ?2",
|
||||
[group_id, project_id],
|
||||
|r| r.get::<_, i64>(0),
|
||||
)
|
||||
.map(|n| n > 0)
|
||||
.unwrap_or(false);
|
||||
if !exists {
|
||||
return Err(format!("项目 {} 不属于产品组 {},或项目不存在", project_id, group_id));
|
||||
}
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT snapshot_date, days_since_commit, blueprint_done_rate,
|
||||
blueprint_in_progress, blueprint_blocked, has_errors
|
||||
FROM project_snapshots
|
||||
WHERE project_id = ?1
|
||||
AND snapshot_date >= date('now', 'localtime', ?2)
|
||||
ORDER BY snapshot_date ASC",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let offset = format!("-{} days", days);
|
||||
let rows: Vec<(String, Option<i64>, Option<f64>, i64, i64, i64)> = stmt
|
||||
.query_map(rusqlite::params![project_id, offset], |r| {
|
||||
Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?, r.get(5)?))
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
if rows.is_empty() {
|
||||
return Ok(format!(
|
||||
"项目 `{}` 暂无历史快照(快照在每次应用启动时写入,最多保留 {} 天)。",
|
||||
project_id, days
|
||||
));
|
||||
}
|
||||
|
||||
let mut md = format!(
|
||||
"# 项目历史快照:`{}`\n\n最近 {} 天,共 {} 条记录\n\n",
|
||||
project_id,
|
||||
days,
|
||||
rows.len()
|
||||
);
|
||||
md.push_str("| 日期 | 距上次提交(天) | 蓝图完成率 | 进行中模块 | 阻塞任务 | 有错误 |\n");
|
||||
md.push_str("|------|--------------|-----------|-----------|---------|------|\n");
|
||||
|
||||
for (date, days_commit, done_rate, in_prog, blocked, has_err) in &rows {
|
||||
let commit_col = days_commit
|
||||
.map(|d| d.to_string())
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let rate_col = done_rate
|
||||
.map(|r| format!("{:.0}%", r * 100.0))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let err_col = if *has_err == 1 { "⚠️" } else { "✅" };
|
||||
md.push_str(&format!(
|
||||
"| {} | {} | {} | {} | {} | {} |\n",
|
||||
date, commit_col, rate_col, in_prog, blocked, err_col
|
||||
));
|
||||
}
|
||||
|
||||
md.push_str("\n---\n\n请根据以上趋势数据,分析哪些变量与项目停滞或健康度下降最相关,并给出改进建议。\n");
|
||||
|
||||
Ok(md)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user