dev-manager-tauri/src-tauri/src/commands/groups.rs
lanrtop 8c06aa5e44 feat: 飞轮智能阶段二完成——MCP SSE 传输 + 复盘覆盖率 + usage.json 扩展采集
- MCP Server 新增 GET/SSE 端点,Claude Code 原生 MCP 打通
- 项目导师面板显示复盘笔记覆盖率统计与缺口提醒
- usage.json 快照新增 complexity_dist、rework_count 等扩展字段
- 独立项目自动创建产品组并注入 MCP
- 新增 retro-catchup 技能用于批量补写历史复盘笔记
- 补全飞轮存量函数单元测试
- NewProjectModal 支持更灵活的模板参数配置
2026-04-13 01:49:56 +09:00

199 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::db::{log_event, pool};
use crate::mcp_inject;
use crate::models::{ProductGroup, ProjectGroupMap};
use rusqlite::params;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct GroupInput {
pub id: Option<String>,
pub name: String,
pub description: Option<String>,
pub space_id: Option<String>,
}
#[tauri::command]
pub fn get_groups(space_id: Option<String>) -> Result<Vec<ProductGroup>, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
let result = match &space_id {
Some(sid) => {
let mut stmt = conn.prepare(
"SELECT * FROM product_groups WHERE space_id = ?1 ORDER BY sort_order ASC, id"
).map_err(|e| e.to_string())?;
let x = stmt.query_map(params![sid], ProductGroup::from_row)
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?; x
}
None => {
let mut stmt = conn.prepare("SELECT * FROM product_groups ORDER BY sort_order ASC, id").map_err(|e| e.to_string())?;
let x = stmt.query_map([], ProductGroup::from_row)
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?; x
}
};
Ok(result)
}
#[tauri::command]
pub fn create_group(input: GroupInput) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
let id = input.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
conn.execute(
"INSERT INTO product_groups (id, name, description, space_id) VALUES (?1, ?2, ?3, ?4)",
params![id, input.name, input.description, input.space_id],
)
.map_err(|e| {
if e.to_string().contains("UNIQUE") { "Group id already exists".into() }
else { e.to_string() }
})?;
Ok(())
}
#[tauri::command]
pub fn update_group(id: String, input: GroupInput) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
conn.execute(
"UPDATE product_groups SET name = COALESCE(?1, name), description = COALESCE(?2, description) WHERE id = ?3",
params![input.name, input.description, id],
)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn delete_group(id: String) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 先清除所有成员的 MCP 配置DB cascade 删除之前)
let member_ids: Vec<String> = conn
.prepare("SELECT project_id FROM project_group_map WHERE group_id = ?1")
.map_err(|e| e.to_string())?
.query_map(params![id], |row| row.get::<_, String>(0))
.map_err(|e| e.to_string())?
.filter_map(|r| r.ok())
.collect();
for project_id in &member_ids {
if let Err(e) = mcp_inject::remove_project_mcp(project_id, &id) {
let ctx = serde_json::json!({"project_id": project_id, "group_id": id}).to_string();
log_event("mcp_inject", "warn", &format!("删除组时清除 MCP 配置失败: {e}"), Some(&ctx));
}
}
conn.execute("DELETE FROM product_groups WHERE id = ?1", params![id])
.map_err(|e| e.to_string())?;
Ok(())
}
/// 按传入的 id 顺序批量更新 sort_orderindex 即为新顺序)
#[tauri::command]
pub fn reorder_groups(ids: Vec<String>) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
for (i, id) in ids.iter().enumerate() {
conn.execute(
"UPDATE product_groups SET sort_order = ?1 WHERE id = ?2",
params![i as i32, id],
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub fn get_group_maps() -> Result<Vec<ProjectGroupMap>, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
let mut stmt = conn.prepare("SELECT * FROM project_group_map").map_err(|e| e.to_string())?;
let result = stmt.query_map([], ProjectGroupMap::from_row)
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string());
result
}
/// 确保独立项目有默认产品组。
/// 若项目已属于某产品组,直接返回;否则创建 "{project_name} (独立项目)" 组,加入成员并注入 MCP。
pub fn ensure_standalone_group(project_id: &str) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 已在某个组内,跳过
let already_in_group: bool = conn
.query_row(
"SELECT COUNT(*) FROM project_group_map WHERE project_id = ?1",
params![project_id],
|row| row.get::<_, i64>(0),
)
.unwrap_or(0)
> 0;
if already_in_group {
return Ok(());
}
// 查项目显示名和所属空间
let (project_name, space_id): (String, Option<String>) = conn
.query_row(
"SELECT pp.name, pw.space_id FROM project_workspaces pw
JOIN project_profiles pp ON pp.id = pw.profile_id
WHERE pw.id = ?1",
params![project_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.map_err(|_| format!("项目 {project_id} 不存在或未关联 profile"))?;
let group_id = uuid::Uuid::new_v4().to_string();
let group_name = format!("{project_name} (独立项目)");
conn.execute(
"INSERT INTO product_groups (id, name, space_id) VALUES (?1, ?2, ?3)",
params![group_id, group_name, space_id],
)
.map_err(|e| e.to_string())?;
conn.execute(
"INSERT OR REPLACE INTO project_group_map (project_id, group_id) VALUES (?1, ?2)",
params![project_id, group_id],
)
.map_err(|e| e.to_string())?;
if let Err(e) = mcp_inject::inject_project_mcp(project_id, &group_id) {
let ctx = serde_json::json!({"project_id": project_id, "group_id": &group_id}).to_string();
log_event("mcp_inject", "warn", &format!("独立项目 MCP 注入失败: {e}"), Some(&ctx));
}
Ok(())
}
#[tauri::command]
pub fn add_group_member(project_id: String, group_id: String, role: Option<String>) -> Result<Option<String>, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
conn.execute(
"INSERT OR REPLACE INTO project_group_map (project_id, group_id, role) VALUES (?1, ?2, ?3)",
params![project_id, group_id, role],
)
.map_err(|e| e.to_string())?;
// DB 写入成功后注入 MCP 配置(非致命,注入失败不影响成员关系)
let inject_warning = match mcp_inject::inject_project_mcp(&project_id, &group_id) {
Ok(()) => None,
Err(e) => {
let ctx = serde_json::json!({"project_id": &project_id, "group_id": &group_id}).to_string();
log_event("mcp_inject", "error", &format!("注入失败: {e}"), Some(&ctx));
Some(e)
}
};
Ok(inject_warning)
}
#[tauri::command]
pub fn remove_group_member(project_id: String, group_id: String) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
// 先清除 MCP 配置(需要在 DB 记录删除前查询项目路径)
if let Err(e) = mcp_inject::remove_project_mcp(&project_id, &group_id) {
let ctx = serde_json::json!({"project_id": &project_id, "group_id": &group_id}).to_string();
log_event("mcp_inject", "warn", &format!("清除 MCP 配置失败: {e}"), Some(&ctx));
}
conn.execute(
"DELETE FROM project_group_map WHERE project_id = ?1 AND group_id = ?2",
params![project_id, group_id],
)
.map_err(|e| e.to_string())?;
Ok(())
}