新增: 仲裁合并(冲突检测+ConflictResolver+reviewer仲裁基础)
- Coordinator.merge: 跳过失败SubTask+同文件冲突检测+extract_written_files - ConflictResolver.vue: 双栏diff展示+接受A/接受B/合并/手动按钮 - i18n: 冲突相关翻译键(中英文) - 7个merge测试全绿(含4个新增冲突检测)
This commit is contained in:
@@ -385,12 +385,16 @@ impl Coordinator {
|
||||
results
|
||||
}
|
||||
|
||||
/// 合并:汇总子结果 → 合并产出
|
||||
/// 合并:汇总子结果 → 合并产出 + 冲突检测
|
||||
///
|
||||
/// Phase 1:简单拼接各 SubTask 产出,每段带标题标识来源。
|
||||
/// `conflicts` 返回空 vec(Phase 2 由 reviewer Agent 仲裁填充)。
|
||||
/// - 成功的 SubTask 产出拼接为 merged_output
|
||||
/// - 失败的 SubTask 跳过(不参与合并)
|
||||
/// - 冲突检测:多个 SubTask 写同一文件路径 → 标记冲突
|
||||
pub fn merge(&self, results: &[ExecutionResult]) -> MergeResult {
|
||||
let parts: Vec<String> = results
|
||||
let success_results: Vec<&ExecutionResult> =
|
||||
results.iter().filter(|r| r.success).collect();
|
||||
|
||||
let parts: Vec<String> = success_results
|
||||
.iter()
|
||||
.map(|r| {
|
||||
format!(
|
||||
@@ -400,11 +404,68 @@ impl Coordinator {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 冲突检测:检查是否有多个 SubTask 改了同一文件
|
||||
let conflicts = self.detect_file_conflicts(results);
|
||||
|
||||
MergeResult {
|
||||
merged_output: parts.join("\n\n---\n\n"),
|
||||
conflicts: Vec::new(),
|
||||
conflicts,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检测文件冲突:从 ExecutionResult 中提取写入的文件路径,同路径 → 冲突
|
||||
/// Phase 1:基于 output 文本中的文件路径关键词(简化检测)
|
||||
/// Phase 2:从 Git worktree 的 git diff 提取精确路径
|
||||
fn detect_file_conflicts(&self, results: &[ExecutionResult]) -> Vec<ConflictItem> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
// 收集每个 SubTask 写入的文件路径(从 output 中提取)
|
||||
let mut file_map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for r in results {
|
||||
if !r.success {
|
||||
continue;
|
||||
}
|
||||
let files = extract_written_files(&r.output);
|
||||
for f in files {
|
||||
file_map.entry(f).or_default().push(r.subtask_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 同文件被多个 SubTask 写 → 冲突
|
||||
let mut conflicts = Vec::new();
|
||||
for (file, subtasks) in &file_map {
|
||||
if subtasks.len() > 1 {
|
||||
conflicts.push(ConflictItem {
|
||||
file: file.clone(),
|
||||
description: format!(
|
||||
"文件 {} 被 {} 个子任务同时修改: {}",
|
||||
file,
|
||||
subtasks.len(),
|
||||
subtasks.join(", ")
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
conflicts
|
||||
}
|
||||
}
|
||||
|
||||
/// 从执行产出文本中提取被写入的文件路径(简化检测:匹配 write_file/patch_file 后的路径)
|
||||
fn extract_written_files(output: &str) -> Vec<String> {
|
||||
let mut files = Vec::new();
|
||||
for line in output.lines() {
|
||||
let trimmed = line.trim();
|
||||
// 匹配 "write_file: path" 或 "patch_file: path" 或 "写入: path" 模式
|
||||
for prefix in ["write_file:", "patch_file:", "写入:", "修改:"] {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix) {
|
||||
let path = rest.trim().split_whitespace().next().unwrap_or("");
|
||||
if !path.is_empty() {
|
||||
files.push(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
// ---- 单元测试 ---------------------------------------------------------------
|
||||
@@ -621,6 +682,107 @@ mod tests {
|
||||
assert!(merged.conflicts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skips_failed_results() {
|
||||
let coord = make_coord();
|
||||
let results = vec![
|
||||
ExecutionResult {
|
||||
subtask_id: "ok".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "成功产出".into(),
|
||||
success: true,
|
||||
},
|
||||
ExecutionResult {
|
||||
subtask_id: "fail".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "执行失败".into(),
|
||||
success: false,
|
||||
},
|
||||
];
|
||||
let merged = coord.merge(&results);
|
||||
assert!(merged.merged_output.contains("成功产出"), "应含成功产出");
|
||||
assert!(!merged.merged_output.contains("执行失败"), "不应含失败产出");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_detects_same_file_conflict() {
|
||||
let coord = make_coord();
|
||||
let results = vec![
|
||||
ExecutionResult {
|
||||
subtask_id: "A".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "write_file: src/main.rs\n新增 auth 模块".into(),
|
||||
success: true,
|
||||
},
|
||||
ExecutionResult {
|
||||
subtask_id: "B".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "write_file: src/main.rs\n新增 payment 模块".into(),
|
||||
success: true,
|
||||
},
|
||||
];
|
||||
let merged = coord.merge(&results);
|
||||
assert_eq!(merged.conflicts.len(), 1, "同文件应检测到 1 个冲突");
|
||||
assert!(merged.conflicts[0].file.contains("main.rs"));
|
||||
assert!(merged.conflicts[0].description.contains("A"));
|
||||
assert!(merged.conflicts[0].description.contains("B"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_no_conflict_different_files() {
|
||||
let coord = make_coord();
|
||||
let results = vec![
|
||||
ExecutionResult {
|
||||
subtask_id: "A".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "write_file: src/auth.rs".into(),
|
||||
success: true,
|
||||
},
|
||||
ExecutionResult {
|
||||
subtask_id: "B".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "write_file: src/payment.rs".into(),
|
||||
success: true,
|
||||
},
|
||||
];
|
||||
let merged = coord.merge(&results);
|
||||
assert!(merged.conflicts.is_empty(), "不同文件不应有冲突");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_conflict_ignored_for_failed() {
|
||||
let coord = make_coord();
|
||||
// 失败的 SubTask 不参与冲突检测
|
||||
let results = vec![
|
||||
ExecutionResult {
|
||||
subtask_id: "A".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "write_file: src/main.rs".into(),
|
||||
success: true,
|
||||
},
|
||||
ExecutionResult {
|
||||
subtask_id: "B".into(),
|
||||
persona_id: "coder".into(),
|
||||
output: "write_file: src/main.rs".into(),
|
||||
success: false, // 失败
|
||||
},
|
||||
];
|
||||
let merged = coord.merge(&results);
|
||||
assert!(merged.conflicts.is_empty(), "失败的不应参与冲突检测");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_files_from_output() {
|
||||
let files = extract_written_files("write_file: src/main.rs\n其他内容");
|
||||
assert_eq!(files, vec!["src/main.rs"]);
|
||||
|
||||
let files = extract_written_files("patch_file: lib/utils.rs\n修改完成");
|
||||
assert!(files.contains(&"lib/utils.rs".to_string()));
|
||||
|
||||
let files = extract_written_files("没有写操作");
|
||||
assert!(files.is_empty());
|
||||
}
|
||||
|
||||
// -- DispatchStrategy --
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user