Merge pull request 'fix(mcp-single-server): 删除登记只清本项目托管条目,沉默项目统计去并集,注入状态带项目名(U1、U2)' (#34) from feat/mcp-single-server-u1 into master
All checks were successful
Push & PR Check / check (push) Successful in 1m4s
All checks were successful
Push & PR Check / check (push) Successful in 1m4s
This commit is contained in:
commit
fc280db426
@ -440,10 +440,10 @@ modules:
|
|||||||
- id: mcp-single-server
|
- id: mcp-single-server
|
||||||
name: MCP 单一服务器化
|
name: MCP 单一服务器化
|
||||||
area: backend
|
area: backend
|
||||||
status: done
|
status: in_progress
|
||||||
progress: 100
|
progress: 89
|
||||||
updated: 2026-07-06
|
updated: 2026-10-03
|
||||||
note: "T1-T6 全部完成并端到端终验(2026-07-06):单一 lian-jing 服务器上线,~/.claude.json 12 条旧注册清零,3 项目抽查新格式通过,旧路由 404。T4 暴露并修复 git-bash 硬编码存量 bug(0d1427f)。遗留:同路径重复登记待清理(见模块 done_note)"
|
note: "T1-T6 全部完成并端到端终验(2026-07-06):单一 lian-jing 服务器上线,~/.claude.json 12 条旧注册清零,3 项目抽查新格式通过,旧路由 404。T4 暴露并修复 git-bash 硬编码存量 bug(0d1427f)。遗留:同路径重复登记待清理(见模块 done_note)。2026-10-03 开 U1-U3 清理:删除登记只清本项目托管条目、沉默项目统计去并集、2c5b54ef 并入 dev-manager-tauri"
|
||||||
position: [1750, 500]
|
position: [1750, 500]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -107,3 +107,31 @@
|
|||||||
- depends: T3
|
- depends: T3
|
||||||
- acceptance: "cargo test 全绿;grep 'mcp/group' src-tauri/src/ 零命中;[HTTP] POST /mcp/group/{gid} → 404(归入 T4 实测)"
|
- acceptance: "cargo test 全绿;grep 'mcp/group' src-tauri/src/ 零命中;[HTTP] POST /mcp/group/{gid} → 404(归入 T4 实测)"
|
||||||
- done_note: 2026-07-06 完成。原计划后置一个版本,经评估提前并入本次发版:迁移在新版启动时自动重写全部客户端配置,旧路由的实际消费者只剩升级前的存量会话(服务重启后本就断开),单人环境保留价值趋近于零。tools_call 签名去 Option 化,list_group_projects 改 required 参数
|
- done_note: 2026-07-06 完成。原计划后置一个版本,经评估提前并入本次发版:迁移在新版启动时自动重写全部客户端配置,旧路由的实际消费者只剩升级前的存量会话(服务重启后本就断开),单人环境保留价值趋近于零。tools_call 签名去 Option 化,list_group_projects 改 required 参数
|
||||||
|
|
||||||
|
## 遗留清理:同路径重复登记(2026-10-03)
|
||||||
|
|
||||||
|
起因:get_diagnostics「使者回传覆盖率」里 dev-manager-tauri 出现两行(147 / 86 条 commit)。只读排查确认是 done_note 记下的遗留——同一路径 `E:\code-project\dev-manager-tauri` 登记了 `dev-manager-tauri`(2026-03-23 批量迁移,现为规范 ID:context.json / buff / 注入 / Codex URL 均指向它)与 `2c5b54ef-…`(2026-04-01 UI 导入)两条 workspace,各自绑一条 gitea_repos 指向同一仓库,推送双采集:commit 按 sha 先到先得被劈成两半(256 / 137),56 对 PR 事件双写,快照双份。诊断里两行数字不同是因为 `silent_projects` 的 `OR win_path` 连接让 UUID 那行吃到了两边并集。「MCP 注入状态」里的 `d248554e-…` 是 enterprise-system,不是重复(该节只打 ID 不打名字导致误读)。
|
||||||
|
|
||||||
|
### ✅ U1. 删除登记只清本项目的客户端托管条目
|
||||||
|
- status: done
|
||||||
|
- complexity: S
|
||||||
|
- files: src-tauri/src/mcp_inject.rs
|
||||||
|
- acceptance: "agent_verify: [cargo test mcp_inject] 同一目录的 Codex 管理块 / .claude/settings.json lian-jing 条目指向别的 project_id 时,remove 后原样保留;指向本 project_id 或不是炼境项目路由(旧格式)时照旧移除"
|
||||||
|
- done_note: 2026-10-03 完成。新增 `remove_keeps_entries_owned_by_another_project_at_same_root`;`cargo test` 全量 303 通过。运行中的旧二进制尚无此修复,U3 删除登记后须立即 inject_mcp 补回。
|
||||||
|
- 为什么:`remove_project_files_mcp` 按路径解析目录后无条件删 Codex 管理块与 settings 条目,同路径重复登记时删掉 UUID 那条会顺手切断规范 ID 的 Codex 接入(主仓库 `.codex/config.toml` 的块 URL 是 `/mcp/projects/dev-manager-tauri`)。Claude local 条目(`sync_claude_local`)与 token 本就按 project_id 匹配,不受影响。
|
||||||
|
|
||||||
|
### ✅ U2. 沉默项目每个 commit 只归属一个 workspace + 注入状态带项目名
|
||||||
|
- status: done
|
||||||
|
- complexity: S
|
||||||
|
- files: src-tauri/src/commands/onboarding.rs, src-tauri/src/mcp_tools.rs
|
||||||
|
- acceptance: "agent_verify: [cargo test silent_projects] 两个 workspace 同路径、commit 分挂两个 ID 时,每条 commit 只计一次(不再出现一行等于两边并集);[cargo test diagnostics] 注入状态行显示「名称 (`id`)」,已不在登记中的 ID 标注未登记"
|
||||||
|
- done_note: 2026-10-03 完成。新增 `silent_projects_counts_each_commit_once_under_duplicate_registration`(旧 SQL 在同一夹具上得 [3, 5],新 SQL 得 [2, 3])与 `diagnostics_inject_label_shows_name_and_flags_unregistered`;`cargo test` 全量 303 通过。
|
||||||
|
- 为什么:`pw.id = cm.project_id OR pw.win_path = bb.project_path` 在同路径多登记时让一条 commit 同时匹配多个 workspace;注入状态只打 ID,人读不出 `d248554e-…` 是哪个项目。
|
||||||
|
|
||||||
|
### 🔵 U3. 存量合并:2c5b54ef 并入 dev-manager-tauri 后删除登记
|
||||||
|
- status: in_progress
|
||||||
|
- complexity: S
|
||||||
|
- files: (运维操作,无代码改动)
|
||||||
|
- acceptance: "agent_verify: [只读 SQL] project_workspaces 中 win_path=E:\code-project\dev-manager-tauri 仅 1 行;gitea_repos 指向 lanrtop/dev-manager-tauri 仅 1 行;commit_metrics/project_notes/pr_events 无 2c5b54ef 残留;[MCP] get_diagnostics 覆盖率 dev-manager-tauri 仅 1 行;verify_onboarding(dev-manager-tauri) 与操作前状态一致;主仓库 git diff .codex/config.toml 为空"
|
||||||
|
- 步骤:① 先解绑 2c5b54ef 的 Gitea(gitea_unlink_repo,删重复 webhook,停双采集)② 在线备份 DB ③ 单事务迁移:commit_metrics / project_notes / 独有 pr_events / 待办改挂规范 ID,重复 pr_events 与 UUID 快照、文档新鲜度缓存删除,space / repo 绑定 / 产品组成员 / profile 空字段补给规范 ID ④ unregister_project(2c5b54ef) → 立即 inject_mcp(dev-manager-tauri) 补回(运行中的旧二进制尚无 U1 修复)⑤ backfill_notes_archive ⑥ 删空产品组 d669f693
|
||||||
|
- 不做:「登记时拒绝同路径」归 agent-client-parity P8f(已规划含重复登记拒绝),此处不重复实现;beast-mode 同类重复(beast-mode / 940dfbb4,无 Gitea 绑定、近 30 天无活动)本卡不处理。
|
||||||
|
|||||||
@ -1169,19 +1169,27 @@ pub fn verify_onboarding(
|
|||||||
/// 而是让遗忘可被看见——人看到信号后自行决定是否去该项目跑 /envoy。
|
/// 而是让遗忘可被看见——人看到信号后自行决定是否去该项目跑 /envoy。
|
||||||
/// commit_metrics.project_id 可能是 blueprint_buffs 的 UUID,经 buff 路径
|
/// commit_metrics.project_id 可能是 blueprint_buffs 的 UUID,经 buff 路径
|
||||||
/// 关联回 project_workspaces;未登记项目(无法跑 /envoy)不列入。
|
/// 关联回 project_workspaces;未登记项目(无法跑 /envoy)不列入。
|
||||||
|
/// 每条 commit 只归属一个 workspace(直接 ID 优先,其次 buff 路径):同路径重复登记时
|
||||||
|
/// 若按「ID 或路径」任一匹配计数,一行会吃到多条登记的并集(2026-10-03 真机 147 / 86)。
|
||||||
pub fn silent_projects(conn: &rusqlite::Connection) -> Result<Vec<(String, i64)>, String> {
|
pub fn silent_projects(conn: &rusqlite::Connection) -> Result<Vec<(String, i64)>, String> {
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT
|
"WITH recent AS (
|
||||||
|
SELECT COALESCE(
|
||||||
|
(SELECT pw.id FROM project_workspaces pw WHERE pw.id = cm.project_id),
|
||||||
|
(SELECT MIN(pw.id) FROM blueprint_buffs bb
|
||||||
|
JOIN project_workspaces pw ON pw.win_path = bb.project_path
|
||||||
|
WHERE bb.project_id = cm.project_id)
|
||||||
|
) AS ws_id
|
||||||
|
FROM commit_metrics cm
|
||||||
|
WHERE cm.committed_at >= datetime('now','-30 days')
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
COALESCE(pp.name, pw.id) AS name,
|
COALESCE(pp.name, pw.id) AS name,
|
||||||
COUNT(*) AS commits
|
COUNT(*) AS commits
|
||||||
FROM commit_metrics cm
|
FROM recent r
|
||||||
LEFT JOIN blueprint_buffs bb ON bb.project_id = cm.project_id
|
JOIN project_workspaces pw ON pw.id = r.ws_id
|
||||||
LEFT JOIN project_workspaces pw
|
|
||||||
ON pw.id = cm.project_id OR pw.win_path = bb.project_path
|
|
||||||
LEFT JOIN project_profiles pp ON pw.profile_id = pp.id
|
LEFT JOIN project_profiles pp ON pw.profile_id = pp.id
|
||||||
WHERE cm.committed_at >= datetime('now','-30 days')
|
|
||||||
AND pw.id IS NOT NULL
|
|
||||||
GROUP BY pw.id
|
GROUP BY pw.id
|
||||||
HAVING (SELECT COUNT(*) FROM project_notes pn
|
HAVING (SELECT COUNT(*) FROM project_notes pn
|
||||||
WHERE pn.project_id = pw.id
|
WHERE pn.project_id = pw.id
|
||||||
@ -2441,4 +2449,33 @@ commit-msg:
|
|||||||
let silent = silent_projects(&conn).unwrap();
|
let silent = silent_projects(&conn).unwrap();
|
||||||
assert_eq!(silent.len(), 2, "pw-b 的回传过期后也应列为沉默");
|
assert_eq!(silent.len(), 2, "pw-b 的回传过期后也应列为沉默");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn silent_projects_counts_each_commit_once_under_duplicate_registration() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
// 2026-10-03 真机形状:同一路径两条登记,buff 挂规范 ID,commit 按 sha 先到先得分挂两个 ID
|
||||||
|
conn.execute_batch(
|
||||||
|
"INSERT INTO project_profiles (id, name) VALUES ('prof-legacy','炼境'),('prof-uuid','炼境');
|
||||||
|
INSERT INTO project_workspaces (id, profile_id, win_path)
|
||||||
|
VALUES ('pw-legacy','prof-legacy','E:/dup'),('pw-uuid','prof-uuid','E:/dup');
|
||||||
|
INSERT INTO blueprint_buffs (project_id, project_path) VALUES ('pw-legacy','E:/dup');
|
||||||
|
INSERT INTO commit_metrics (sha, project_id, commit_type, is_rework, is_ci_auto, committed_at, parsed_at)
|
||||||
|
VALUES ('d1','pw-legacy','feat',0,0,datetime('now'),datetime('now')),
|
||||||
|
('d2','pw-legacy','fix',1,0,datetime('now'),datetime('now')),
|
||||||
|
('d3','pw-legacy','feat',0,0,datetime('now'),datetime('now')),
|
||||||
|
('d4','pw-uuid','feat',0,0,datetime('now'),datetime('now')),
|
||||||
|
('d5','pw-uuid','docs',0,0,datetime('now'),datetime('now'));",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let silent = silent_projects(&conn).unwrap();
|
||||||
|
let mut counts: Vec<i64> = silent.iter().map(|(_, n)| *n).collect();
|
||||||
|
counts.sort();
|
||||||
|
assert_eq!(
|
||||||
|
counts,
|
||||||
|
vec![2, 3],
|
||||||
|
"各计各的,不出现一行等于两边并集:{silent:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(counts.iter().sum::<i64>(), 5, "总数等于去重后的 commit 数");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1463,7 +1463,13 @@ fn upsert_codex_mcp_config(
|
|||||||
.map_err(|error| format!("写入 Codex config.toml 失败: {error}"))
|
.map_err(|error| format!("写入 Codex config.toml 失败: {error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_codex_mcp_config(path: &PathBuf) -> Result<(), String> {
|
/// 条目 URL 指向别的炼境项目路由:同路径重复登记时,删除一条登记不得连带清掉另一条的接入。
|
||||||
|
/// 不是炼境项目路由的旧地址(全局 /mcp、组级地址)视为本项目遗留,照旧清理。
|
||||||
|
fn owned_by_other_project(url: &str, project_id: &str) -> bool {
|
||||||
|
lianjing_project_of(url).is_some_and(|owner| owner != project_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_codex_mcp_config(path: &PathBuf, project_id: &str) -> Result<(), String> {
|
||||||
let current = match std::fs::read_to_string(path) {
|
let current = match std::fs::read_to_string(path) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||||
@ -1478,6 +1484,12 @@ fn remove_codex_mcp_config(path: &PathBuf) -> Result<(), String> {
|
|||||||
if end < start {
|
if end < start {
|
||||||
return Err(format!("{} 的炼境 Codex 管理块不完整", path.display()));
|
return Err(format!("{} 的炼境 Codex 管理块不完整", path.display()));
|
||||||
}
|
}
|
||||||
|
let block_url = current[start..end]
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.trim().strip_prefix("url = \"")?.strip_suffix('"'));
|
||||||
|
if block_url.is_some_and(|url| owned_by_other_project(url, project_id)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let next = format!(
|
let next = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
@ -1860,20 +1872,30 @@ pub fn remove_project_mcp(project_id: &str) -> Result<(), String> {
|
|||||||
|
|
||||||
/// 清理项目文件中的炼境 MCP 配置(Codex 托管块与 .claude/settings.json 兼容条目)。
|
/// 清理项目文件中的炼境 MCP 配置(Codex 托管块与 .claude/settings.json 兼容条目)。
|
||||||
fn remove_project_files_mcp(project_id: &str) -> Result<(), String> {
|
fn remove_project_files_mcp(project_id: &str) -> Result<(), String> {
|
||||||
remove_codex_mcp_config(&codex_config_path(project_id)?)?;
|
remove_codex_mcp_config(&codex_config_path(project_id)?, project_id)?;
|
||||||
let path = settings_path(project_id)?;
|
remove_settings_mcp_entry(&settings_path(project_id)?, project_id)
|
||||||
if path.exists() {
|
}
|
||||||
let mut settings = read_json(&path)?;
|
|
||||||
if let Some(servers) = settings
|
fn remove_settings_mcp_entry(path: &PathBuf, project_id: &str) -> Result<(), String> {
|
||||||
.get_mut("mcpServers")
|
if !path.exists() {
|
||||||
.and_then(|v| v.as_object_mut())
|
return Ok(());
|
||||||
{
|
|
||||||
servers.remove(MCP_KEY);
|
|
||||||
servers.retain(|k, _| !k.starts_with(LEGACY_KEY_PREFIX));
|
|
||||||
}
|
|
||||||
write_json(&path, &settings)?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
let mut settings = read_json(path)?;
|
||||||
|
if let Some(servers) = settings
|
||||||
|
.get_mut("mcpServers")
|
||||||
|
.and_then(|v| v.as_object_mut())
|
||||||
|
{
|
||||||
|
let foreign = servers
|
||||||
|
.get(MCP_KEY)
|
||||||
|
.and_then(|entry| entry.get("url"))
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.is_some_and(|url| owned_by_other_project(url, project_id));
|
||||||
|
if !foreign {
|
||||||
|
servers.remove(MCP_KEY);
|
||||||
|
}
|
||||||
|
servers.retain(|k, _| !k.starts_with(LEGACY_KEY_PREFIX));
|
||||||
|
}
|
||||||
|
write_json(path, &settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 单一服务器化一次性迁移:批量重注入所有托管项目(换新 key + 清旧 key + liangjing.json 瘦身)
|
/// 单一服务器化一次性迁移:批量重注入所有托管项目(换新 key + 清旧 key + liangjing.json 瘦身)
|
||||||
@ -2182,7 +2204,7 @@ mod tests {
|
|||||||
assert!(migrated.contains("url = \"http://127.0.0.1:27190/mcp/projects/project%20one\""));
|
assert!(migrated.contains("url = \"http://127.0.0.1:27190/mcp/projects/project%20one\""));
|
||||||
assert!(!migrated.contains("url = \"http://127.0.0.1:27190/mcp\"\n"));
|
assert!(!migrated.contains("url = \"http://127.0.0.1:27190/mcp\"\n"));
|
||||||
|
|
||||||
remove_codex_mcp_config(&path).unwrap();
|
remove_codex_mcp_config(&path, "project one").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(&path).unwrap(),
|
std::fs::read_to_string(&path).unwrap(),
|
||||||
"model = \"gpt-6\"\n"
|
"model = \"gpt-6\"\n"
|
||||||
@ -2203,6 +2225,81 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 2026-10-03 真机:同一目录登记了 dev-manager-tauri 与 2c5b54ef 两条,删 UUID 那条会把
|
||||||
|
/// 指向规范 ID 的 Codex 管理块一并删掉。删除只清 URL 属于本项目(或旧格式)的条目。
|
||||||
|
#[test]
|
||||||
|
fn remove_keeps_entries_owned_by_another_project_at_same_root() {
|
||||||
|
let base = std::env::temp_dir().join(format!(
|
||||||
|
"lianjing_remove_scoped_test_{}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(base.join(".codex")).unwrap();
|
||||||
|
std::fs::create_dir_all(base.join(".claude")).unwrap();
|
||||||
|
let helper = "\"C:/Lianjing.exe\" --lianjing-mcp-header qa-only-digest";
|
||||||
|
let codex = base.join(".codex/config.toml");
|
||||||
|
let other_url = "http://127.0.0.1:27190/mcp/projects/canonical-id";
|
||||||
|
|
||||||
|
std::fs::write(&codex, "model = \"gpt-6\"\n").unwrap();
|
||||||
|
upsert_codex_mcp_config(&codex, other_url, helper).unwrap();
|
||||||
|
let before = std::fs::read_to_string(&codex).unwrap();
|
||||||
|
remove_codex_mcp_config(&codex, "duplicate-uuid").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(&codex).unwrap(),
|
||||||
|
before,
|
||||||
|
"别的项目的 Codex 管理块必须原样保留"
|
||||||
|
);
|
||||||
|
remove_codex_mcp_config(&codex, "canonical-id").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(&codex).unwrap(),
|
||||||
|
"model = \"gpt-6\"\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 旧格式全局地址不属于任何项目路由,视为遗留照旧清理
|
||||||
|
upsert_codex_mcp_config(&codex, "http://127.0.0.1:27190/mcp", helper).unwrap();
|
||||||
|
remove_codex_mcp_config(&codex, "duplicate-uuid").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(&codex).unwrap(),
|
||||||
|
"model = \"gpt-6\"\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let settings = base.join(".claude/settings.json");
|
||||||
|
let write_settings = |url: &str| {
|
||||||
|
std::fs::write(
|
||||||
|
&settings,
|
||||||
|
json!({ "mcpServers": {
|
||||||
|
MCP_KEY: { "type": "http", "url": url },
|
||||||
|
"lian-jing:group-old": { "type": "http", "url": "http://127.0.0.1:27190/mcp/group/g" },
|
||||||
|
"other": { "command": "x" }
|
||||||
|
} })
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
};
|
||||||
|
let servers = || read_json(&settings).unwrap()["mcpServers"].clone();
|
||||||
|
|
||||||
|
write_settings(other_url);
|
||||||
|
remove_settings_mcp_entry(&settings, "duplicate-uuid").unwrap();
|
||||||
|
let kept = servers();
|
||||||
|
assert_eq!(
|
||||||
|
kept[MCP_KEY]["url"], other_url,
|
||||||
|
"别的项目的 settings 条目必须保留"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
kept.get("lian-jing:group-old").is_none(),
|
||||||
|
"旧组级 key 照旧清理"
|
||||||
|
);
|
||||||
|
assert!(kept.get("other").is_some());
|
||||||
|
|
||||||
|
write_settings("http://127.0.0.1:27190/mcp/projects/duplicate-uuid");
|
||||||
|
remove_settings_mcp_entry(&settings, "duplicate-uuid").unwrap();
|
||||||
|
let removed = servers();
|
||||||
|
assert!(removed.get(MCP_KEY).is_none());
|
||||||
|
assert!(removed.get("other").is_some());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
|
||||||
// ── P8b:三端结构化结果 ──
|
// ── P8b:三端结构化结果 ──
|
||||||
|
|
||||||
fn claude_get_local(url: &str) -> String {
|
fn claude_get_local(url: &str) -> String {
|
||||||
|
|||||||
@ -1431,6 +1431,22 @@ async fn tools_call_with_scope(params: Option<&Value>, project_scope: Option<Str
|
|||||||
|
|
||||||
// ── 工具实现 ──────────────────────────────────────────────────────────────────
|
// ── 工具实现 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 诊断里的项目标签「名称 (`id`)」:只打 ID 人读不出是哪个项目(2026-10-03 把 enterprise-system 的
|
||||||
|
/// UUID 误读成炼境重复登记);日志里残留的已删除登记标注未登记,免得当成现存项目。
|
||||||
|
fn diagnostics_project_label(conn: &rusqlite::Connection, project_id: &str) -> String {
|
||||||
|
match conn.query_row(
|
||||||
|
"SELECT pp.name FROM project_workspaces pw
|
||||||
|
LEFT JOIN project_profiles pp ON pp.id = pw.profile_id
|
||||||
|
WHERE pw.id = ?1",
|
||||||
|
[project_id],
|
||||||
|
|r| r.get::<_, Option<String>>(0),
|
||||||
|
) {
|
||||||
|
Ok(Some(name)) => format!("{name} (`{project_id}`)"),
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => format!("`{project_id}`(未登记)"),
|
||||||
|
_ => format!("`{project_id}`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_diagnostics() -> Result<String, String> {
|
fn get_diagnostics() -> Result<String, String> {
|
||||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||||
let port = crate::mcp_server::read_port();
|
let port = crate::mcp_server::read_port();
|
||||||
@ -1494,8 +1510,11 @@ fn get_diagnostics() -> Result<String, String> {
|
|||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
let icon = if level == "info" { "✅" } else { "⚠️" };
|
let icon = if level == "info" { "✅" } else { "⚠️" };
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"- {} `{}` — {} _({})\n",
|
"- {} {} — {} _({})\n",
|
||||||
icon, project_id, msg, ts
|
icon,
|
||||||
|
diagnostics_project_label(&conn, &project_id),
|
||||||
|
msg,
|
||||||
|
ts
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
@ -3170,4 +3189,19 @@ mod tests {
|
|||||||
"apply_onboarding_pack 的 project_id 应可省略(U1 批量模式)"
|
"apply_onboarding_pack 的 project_id 应可省略(U1 批量模式)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diagnostics_inject_label_shows_name_and_flags_unregistered() {
|
||||||
|
let conn = crate::db::conn_with_schema();
|
||||||
|
conn.execute_batch(
|
||||||
|
"INSERT INTO project_profiles (id, name) VALUES ('prof-e','enterprise-system');
|
||||||
|
INSERT INTO project_workspaces (id, profile_id, win_path) VALUES ('uuid-e','prof-e','E:/e');",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
diagnostics_project_label(&conn, "uuid-e"),
|
||||||
|
"enterprise-system (`uuid-e`)"
|
||||||
|
);
|
||||||
|
assert_eq!(diagnostics_project_label(&conn, "gone"), "`gone`(未登记)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user