From 1d580dccc340d9ec5d4a387abbaf0409e625705d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 1 Jul 2026 22:32:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E4=BB=B2=E8=A3=81?= =?UTF-8?q?=E5=90=88=E5=B9=B6(=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B+Conflic?= =?UTF-8?q?tResolver+reviewer=E4=BB=B2=E8=A3=81=E5=9F=BA=E7=A1=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Coordinator.merge: 跳过失败SubTask+同文件冲突检测+extract_written_files - ConflictResolver.vue: 双栏diff展示+接受A/接受B/合并/手动按钮 - i18n: 冲突相关翻译键(中英文) - 7个merge测试全绿(含4个新增冲突检测) --- crates/df-ai/src/coordinator.rs | 172 ++++++++++++++++++++++++- src/api/types.ts | 2 + src/components/ai/ConflictResolver.vue | 119 +++++++++++++++++ src/i18n/en/aiChat.ts | 5 + src/i18n/zh-CN/aiChat.ts | 5 + 5 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 src/components/ai/ConflictResolver.vue diff --git a/crates/df-ai/src/coordinator.rs b/crates/df-ai/src/coordinator.rs index 3f3a6f9..1fb6fc5 100644 --- a/crates/df-ai/src/coordinator.rs +++ b/crates/df-ai/src/coordinator.rs @@ -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 = results + let success_results: Vec<&ExecutionResult> = + results.iter().filter(|r| r.success).collect(); + + let parts: Vec = 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 { + use std::collections::HashMap; + + // 收集每个 SubTask 写入的文件路径(从 output 中提取) + let mut file_map: HashMap> = 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 { + 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] diff --git a/src/api/types.ts b/src/api/types.ts index c01d94b..609a350 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -811,4 +811,6 @@ export interface ConflictInfo { conflict_type: ConflictType subtask_a: string | null subtask_b: string | null + diff_a?: string | null + diff_b?: string | null } diff --git a/src/components/ai/ConflictResolver.vue b/src/components/ai/ConflictResolver.vue new file mode 100644 index 0000000..20ee435 --- /dev/null +++ b/src/components/ai/ConflictResolver.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts index 978b8c6..cc3a156 100644 --- a/src/i18n/en/aiChat.ts +++ b/src/i18n/en/aiChat.ts @@ -3,6 +3,11 @@ export default { // ── Multi-Agent parallel execution ── planProgress: 'Execution Plan', planLayer: 'Layer {n}', + conflictsPending: '{n} conflicts to resolve', + acceptA: 'Accept A', + acceptB: 'Accept B', + mergeBoth: 'Merge Both', + manualResolve: 'Manual', // ── Conversation sidebar ── sidebarTitle: 'Chats', diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts index 08bdba4..0e38c96 100644 --- a/src/i18n/zh-CN/aiChat.ts +++ b/src/i18n/zh-CN/aiChat.ts @@ -3,6 +3,11 @@ export default { // ── 多 Agent 并行执行 ── planProgress: '执行计划', planLayer: '第 {n} 层', + conflictsPending: '{n} 处冲突待处理', + acceptA: '接受 A', + acceptB: '接受 B', + mergeBoth: '合并两者', + manualResolve: '手动处理', // ── 对话侧栏 ── sidebarTitle: '对话',