All checks were successful
Push & PR Check / check (push) Successful in 1m4s
T1 服务器层:新增全局 /mcp + /mcp/messages 路由,process_rpc 组上下文 Option 化;旧 /mcp/group/:gid 路由保留过渡,T6 移除。驻军测试 2 条。 T2 工具层:resolve_project_root 按 project_id 直查 project_workspaces, 删除 3 处组归属校验(实测无隔离价值);list_group_projects 改显式传参。 T3 注入层:settings.json 收敛为单 "lian-jing" key(URL /mcp),注入时 清理旧 lian-jing:group-* key;liangjing.json 主动删除 group_id; 启动时一次性迁移(重注入全量 + 清理旧全局注册两种命名格式,标记位防重)。 T5 同步:git-workflow 文档/模板、retro-catchup 技能 Python 兜底、 设置页 URL 展示、mcp-config-inject 模块取代注记。 收益:MCP 客户端配置成本 O(N组)→O(1),授权一次永久生效; group_id 缺失/过期/移组同步一整类问题从根上消失。
219 lines
8.2 KiB
Rust
219 lines
8.2 KiB
Rust
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 {
|
||
// 单一 lian-jing key 为全项目共用:成员还属于其他组时保留配置
|
||
let in_other_group: i64 = conn
|
||
.query_row(
|
||
"SELECT COUNT(*) FROM project_group_map WHERE project_id = ?1 AND group_id != ?2",
|
||
params![project_id, id],
|
||
|row| row.get(0),
|
||
)
|
||
.unwrap_or(0);
|
||
if in_other_group > 0 {
|
||
continue;
|
||
}
|
||
if let Err(e) = mcp_inject::remove_project_mcp(project_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_order(index 即为新顺序)
|
||
#[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) {
|
||
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) {
|
||
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 配置(单一 lian-jing key 为全项目共用)
|
||
let in_other_group: i64 = conn
|
||
.query_row(
|
||
"SELECT COUNT(*) FROM project_group_map WHERE project_id = ?1 AND group_id != ?2",
|
||
params![project_id, group_id],
|
||
|row| row.get(0),
|
||
)
|
||
.unwrap_or(0);
|
||
if in_other_group == 0 {
|
||
if let Err(e) = mcp_inject::remove_project_mcp(&project_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(())
|
||
}
|