import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import '../app/app_store_policy.dart'; import 'aris_bundle.dart'; import 'go_core.dart'; import 'aris_llm_chat_client.dart'; import 'multi_agent_frameworks.dart'; import 'runtime_models.dart'; typedef CliProcessStarter = Future Function( String executable, List arguments, { Map? environment, String? workingDirectory, }); /// 多 Agent 协作编排器 /// /// 管理 Architect(调度/文档)→ Lead Engineer(主程)→ Worker/Review(并行 worker + 复审) /// 的工作流,通过 Ollama 与外部 CLI 工具桥接首批云模型协作能力。 /// /// 角色分工: /// - Architect(调度/文档):负责任务分解、接受标准、工作流设计 /// - Lead Engineer(主程):负责关键实现、重构、集成收口 /// - Worker/Review(并行 worker):负责补充实现、复审、回归建议 class MultiAgentOrchestrator extends ChangeNotifier { MultiAgentOrchestrator({ required MultiAgentConfig config, ArisBundleRepository? arisBundleRepository, GoCoreLocator? goCoreLocator, Future Function(String command)? binaryExistsResolver, HttpClient Function()? httpClientFactory, ArisLlmChatClient? arisLlmChatClient, CliProcessStarter? processStarter, }) : _config = config, _arisBundleRepository = arisBundleRepository ?? ArisBundleRepository(), _goCoreLocator = goCoreLocator ?? GoCoreLocator(binaryExistsResolver: binaryExistsResolver), _binaryExistsResolver = binaryExistsResolver, _httpClientFactory = httpClientFactory ?? HttpClient.new, _processStarter = processStarter ?? ((executable, arguments, {environment, workingDirectory}) { return Process.start( executable, arguments, environment: environment, workingDirectory: workingDirectory, ); }), _arisLlmChatClient = arisLlmChatClient ?? ArisLlmChatClient( bridgeLocator: goCoreLocator ?? GoCoreLocator(binaryExistsResolver: binaryExistsResolver), ); /// 当前配置 MultiAgentConfig _config; MultiAgentConfig get config => _config; final ArisBundleRepository _arisBundleRepository; final GoCoreLocator _goCoreLocator; final Future Function(String command)? _binaryExistsResolver; final HttpClient Function() _httpClientFactory; final CliProcessStarter _processStarter; final ArisLlmChatClient _arisLlmChatClient; Process? _activeCliProcess; HttpClient? _activeHttpClient; bool _abortRequested = false; /// 协作模式是否启用 bool _collaborationEnabled = false; bool get collaborationEnabled => _collaborationEnabled; /// 是否正在运行 bool _isRunning = false; bool get isRunning => _isRunning; /// 最后错误 String? _lastError; String? get lastError => _lastError; /// 当前迭代轮次 int _currentIteration = 0; int get currentIteration => _currentIteration; /// 状态日志 final List _logEntries = []; List get logEntries => List.unmodifiable(_logEntries); /// 更新配置 void updateConfig(MultiAgentConfig config) { _config = config; _collaborationEnabled = config.enabled; notifyListeners(); } Future abort() async { _abortRequested = true; final process = _activeCliProcess; _activeCliProcess = null; if (process != null) { try { process.kill(); } catch (_) { // Best effort only. } } final client = _activeHttpClient; _activeHttpClient = null; if (client != null) { try { client.close(force: true); } catch (_) { // Best effort only. } } } void _assertEmbeddedProcessesAllowed() { if (blocksAppStoreEmbeddedAgentProcesses( isAppleHost: Platform.isIOS || Platform.isMacOS, )) { throw UnsupportedError( 'App Store builds do not allow launching embedded multi-agent subprocesses.', ); } } /// 启用协作模式 void enable() { _config = _config.copyWith(enabled: true); _collaborationEnabled = true; _lastError = null; notifyListeners(); } /// 禁用协作模式 void disable() { _config = _config.copyWith(enabled: false); _collaborationEnabled = false; notifyListeners(); } /// 切换协作模式 void toggle() { if (_collaborationEnabled) { disable(); } else { enable(); } } /// 执行完整的协作工作流 /// /// 流程:Architect 分析 → Engineer 实现 → Tester 审阅 → 迭代(如需要) Future runCollaboration({ required String taskPrompt, required String workingDirectory, List attachments = const [], List selectedSkills = const [], String aiGatewayBaseUrl = '', String aiGatewayApiKey = '', void Function(MultiAgentRunEvent event)? onEvent, }) async { _assertEmbeddedProcessesAllowed(); if (_isRunning) { throw StateError('Collaboration is already running'); } _isRunning = true; _currentIteration = 0; _abortRequested = false; _logEntries.clear(); _lastError = null; notifyListeners(); final startTime = DateTime.now(); final steps = []; final preset = _config.usesAris ? ArisFrameworkPreset(_arisBundleRepository) : const NativeFrameworkPreset(); try { // === Phase 1: Architect 分析任务 === _throwIfAborted(); _log( CollaborationLogLevel.info, '🎨', '${_roleLabel(MultiAgentRole.architect)} 开始分析任务...', ); _emitEvent( onEvent, MultiAgentRunEvent( type: 'step', title: _roleLabel(MultiAgentRole.architect), message: '${_roleLabel(MultiAgentRole.architect)} 开始分析任务…', pending: true, error: false, role: 'architect', ), ); final architectResult = await _runArchitect( taskPrompt, preset: preset, selectedSkills: selectedSkills, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( role: 'architect', status: StepStatus.completed, output: architectResult.output, duration: architectResult.duration, ), ); _emitEvent( onEvent, MultiAgentRunEvent( type: 'step', title: _roleLabel(MultiAgentRole.architect), message: '完成任务分析并生成执行分解。', pending: false, error: false, role: 'architect', data: { 'taskCount': architectResult.decomposedTasks.length, }, ), ); // === Phase 2: Engineer 实现 === _throwIfAborted(); _log( CollaborationLogLevel.info, '🔧', '${_roleLabel(MultiAgentRole.engineer)} 开始实现...', ); _emitEvent( onEvent, MultiAgentRunEvent( type: 'step', title: _roleLabel(MultiAgentRole.engineer), message: '${_roleLabel(MultiAgentRole.engineer)} 开始实现任务…', pending: true, error: false, role: 'engineer', ), ); final engineerResult = await _runEngineer( architectResult.decomposedTasks, workingDirectory, attachments, preset: preset, selectedSkills: selectedSkills, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( role: 'engineer', status: StepStatus.completed, output: engineerResult.output, duration: engineerResult.duration, ), ); _emitEvent( onEvent, MultiAgentRunEvent( type: 'step', title: _roleLabel(MultiAgentRole.engineer), message: '完成首轮实现。', pending: false, error: false, role: 'engineer', ), ); // === Phase 3: Tester 审阅 === _throwIfAborted(); _log( CollaborationLogLevel.info, '🔍', '${_roleLabel(MultiAgentRole.testerDoc)} 开始审阅...', ); _emitEvent( onEvent, MultiAgentRunEvent( type: 'step', title: _roleLabel(MultiAgentRole.testerDoc), message: '${_roleLabel(MultiAgentRole.testerDoc)} 开始审阅实现…', pending: true, error: false, role: 'tester', ), ); final testerResult = await _runTester( engineerResult.codeOutput, preset: preset, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( role: 'tester', status: StepStatus.completed, output: testerResult.output, duration: testerResult.duration, score: testerResult.score, ), ); _emitEvent( onEvent, MultiAgentRunEvent( type: 'step', title: _roleLabel(MultiAgentRole.testerDoc), message: '完成代码审阅。', pending: false, error: false, role: 'tester', score: testerResult.score, ), ); // === Phase 4: 迭代审阅循环(如需要)=== if (testerResult.score < _config.minAcceptableScore) { _log( CollaborationLogLevel.warning, '⚠️', '质量评分 ${testerResult.score}/10 未达标,开始迭代审阅...', ); for (var i = 0; i < _config.maxIterations; i++) { _throwIfAborted(); _currentIteration = i + 1; _log( CollaborationLogLevel.info, '🔄', '迭代 $_currentIteration/${_config.maxIterations}...', ); notifyListeners(); // Lead Engineer 接收反馈并修复 final fixedResult = await _runFix( engineerResult.codeOutput, testerResult.feedback, workingDirectory, preset: preset, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( role: 'engineer', status: StepStatus.completed, output: fixedResult.output, duration: fixedResult.duration, iteration: _currentIteration, ), ); // Tester 重新审阅 final reReview = await _runTester( fixedResult.codeOutput, preset: preset, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); steps.add( CollaborationStep( role: 'tester', status: StepStatus.completed, output: reReview.output, duration: reReview.duration, score: reReview.score, iteration: _currentIteration, ), ); if (reReview.score >= _config.minAcceptableScore) { _log( CollaborationLogLevel.success, '✅', '质量达标 (${reReview.score}/10),迭代结束', ); engineerResult.codeOutput = fixedResult.codeOutput; break; } else if (_currentIteration >= _config.maxIterations) { _log( CollaborationLogLevel.error, '❌', '达到最大迭代次数 ${_config.maxIterations},质量仍未达标', ); } } } else { _log( CollaborationLogLevel.success, '✅', '质量达标 (${testerResult.score}/10),无需迭代', ); } final duration = DateTime.now().difference(startTime); _isRunning = false; notifyListeners(); return CollaborationResult( success: true, steps: steps, finalCode: engineerResult.codeOutput, finalScore: testerResult.score, duration: duration, iterations: _currentIteration, ); } catch (e) { _lastError = e.toString(); _log(CollaborationLogLevel.error, '❌', '协作失败: $_lastError'); _isRunning = false; notifyListeners(); return CollaborationResult( success: false, steps: steps, finalCode: '', finalScore: 0, duration: DateTime.now().difference(startTime), iterations: _currentIteration, error: _lastError, ); } } /// 运行 Architect(调度/文档分析) Future _runArchitect( String task, { required FrameworkPreset preset, required List selectedSkills, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); try { // 根据配置选择 Architect 工具 if (_config.architectEnabled) { final tool = await _resolveToolForRole( MultiAgentRole.architect, _config.architectTool, ); final instructionBlock = await preset.roleInstructionBlock( role: MultiAgentRole.architect, tool: tool, selectedSkills: selectedSkills, ); final result = await _runCliPrompt( role: MultiAgentRole.architect, tool: tool, model: _resolvedModelForRole( MultiAgentRole.architect, configuredModel: _config.architectModel, ), prompt: _buildArchitectPrompt(task, selectedSkills, instructionBlock), cwd: '', aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); // 解析分解后的任务 final tasks = _parseDecomposedTasks(result.output); return ArchitectResult( output: result.output, decomposedTasks: tasks, duration: stopwatch.elapsed, ); } else { // Architect 被禁用,直接返回原任务作为单一子任务 stopwatch.stop(); return ArchitectResult( output: task, decomposedTasks: [ SubTask( id: '1', description: task, order: 1, type: SubTaskType.implementation, ), ], duration: stopwatch.elapsed, ); } } catch (e) { stopwatch.stop(); rethrow; } } /// 运行 Lead Engineer(主实现) Future _runEngineer( List tasks, String workingDirectory, List attachments, { required FrameworkPreset preset, required List selectedSkills, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); final tool = await _resolveToolForRole( MultiAgentRole.engineer, _config.engineerTool, ); final instructionBlock = await preset.roleInstructionBlock( role: MultiAgentRole.engineer, tool: tool, selectedSkills: selectedSkills, ); final taskList = tasks .map((t) => '## ${t.order}. ${t.description}') .join('\n\n'); final prompt = ''' $instructionBlock 你是一个资深工程师,负责完成以下编码任务: ### 任务列表 $taskList ### 工作目录 $workingDirectory ### 附件信息 ${attachments.map((a) => '- ${a.name}: ${a.description}').join('\n')} ### 优先技能 ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').join('\n')} 请完成这些任务,输出完整的代码实现。 '''; final result = await _runCliPrompt( role: MultiAgentRole.engineer, tool: tool, model: _resolvedModelForRole( MultiAgentRole.engineer, configuredModel: _config.engineerModel, ), prompt: prompt, cwd: workingDirectory, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); return EngineerResult( output: result.output, codeOutput: result.output, completedTasks: tasks, duration: stopwatch.elapsed, ); } /// 运行 Worker/Review(代码审阅) Future _runTester( String codeOutput, { required FrameworkPreset preset, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); final tool = await _resolveToolForRole( MultiAgentRole.testerDoc, _config.testerTool, ); final instructionBlock = await preset.roleInstructionBlock( role: MultiAgentRole.testerDoc, tool: tool, selectedSkills: const [], ); final prompt = ''' $instructionBlock 请审阅以下代码,并按以下格式输出: ## 评分 (1-10) [1-10 的分数,10 最高] ## 问题列表 [发现的问题,格式:- 问题描述 (严重程度: 高/中/低)] ## 改进建议 [具体的改进建议] ## 测试用例 ```[语言] [生成的测试用例代码] ``` ## 文档建议 [如有需要补充的文档说明] ### 待审阅代码 ${codeOutput.length > 4000 ? '${codeOutput.substring(0, 4000)}\n...[代码已截断]' : codeOutput} '''; final testerModel = _resolvedModelForRole( MultiAgentRole.testerDoc, configuredModel: _config.testerModel, ); final result = _config.usesAris && tool == 'claude' ? await _runArisTesterViaClaudeReview( model: testerModel, prompt: prompt, ) : await _runCliPrompt( role: MultiAgentRole.testerDoc, tool: tool, model: testerModel, prompt: prompt, cwd: '', aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); final score = _parseReviewScore(result.output); final feedback = _extractFeedback(result.output); return TesterResult( output: result.output, score: score, feedback: feedback, duration: stopwatch.elapsed, ); } /// 运行修复(迭代循环中) Future _runFix( String originalCode, String feedback, String workingDirectory, { required FrameworkPreset preset, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { final stopwatch = Stopwatch()..start(); final tool = await _resolveToolForRole( MultiAgentRole.engineer, _config.engineerTool, ); final instructionBlock = await preset.roleInstructionBlock( role: MultiAgentRole.engineer, tool: tool, selectedSkills: const [], ); final prompt = ''' $instructionBlock 你是一个资深工程师。请根据审阅反馈修复代码。 ## 审阅反馈 $feedback ## 原始代码 $originalCode 请完成修复,输出修复后的完整代码。 '''; final result = await _runCliPrompt( role: MultiAgentRole.engineer, tool: tool, model: _resolvedModelForRole( MultiAgentRole.engineer, configuredModel: _config.engineerModel, ), prompt: prompt, cwd: workingDirectory, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); stopwatch.stop(); return EngineerResult( output: result.output, codeOutput: result.output, completedTasks: [], duration: stopwatch.elapsed, ); } /// 通用的 CLI 进程执行方法 Future _runCliPrompt({ required MultiAgentRole role, required String tool, required String model, required String prompt, required String cwd, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { late final List args; late final String command; late final Map envVars; final useOllamaLaunch = _prefersOllamaLaunch(tool: tool, model: model); switch (tool) { case 'claude': command = useOllamaLaunch ? 'ollama' : _resolveCliPath('claude'); envVars = _buildCliEnvVars( tool: tool, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); if (useOllamaLaunch) { args = _buildOllamaLaunchArgs( tool: tool, model: model, prompt: prompt, cwd: cwd, ); } else if (model.isNotEmpty) { args = ['--model', model, '-p', prompt]; } else { args = ['-p', prompt]; } break; case 'codex': command = useOllamaLaunch ? 'ollama' : _resolveCliPath('codex'); envVars = _buildCliEnvVars( tool: tool, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); if (useOllamaLaunch) { args = _buildOllamaLaunchArgs( tool: tool, model: model, prompt: prompt, cwd: cwd, ); } else if (model.isNotEmpty) { args = [ 'exec', '--skip-git-repo-check', '--color', 'never', if (cwd.isNotEmpty) ...['-C', cwd], '-m', model, prompt, ]; } else { args = [ 'exec', '--skip-git-repo-check', '--color', 'never', if (cwd.isNotEmpty) ...['-C', cwd], prompt, ]; } break; case 'gemini': command = _resolveCliPath('gemini'); envVars = _buildCliEnvVars( tool: tool, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); if (model.isNotEmpty) { args = ['--model', model, '-p', prompt]; } else { args = ['-p', prompt]; } break; case 'opencode': command = useOllamaLaunch ? 'ollama' : _resolveCliPath('opencode'); envVars = _buildCliEnvVars( tool: tool, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); args = useOllamaLaunch ? _buildOllamaLaunchArgs( tool: tool, model: model, prompt: prompt, cwd: cwd, ) : [ 'run', '--format', 'default', if (cwd.isNotEmpty) ...['--dir', cwd], if (model.isNotEmpty) ...['-m', model], prompt, ]; break; default: throw ArgumentError('Unknown tool: $tool'); } final cliAvailable = await _binaryExists(command); if (_config.usesAris && !cliAvailable) { return _runArisFallback( role: role, model: model, prompt: prompt, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); } try { final process = await _processStarter( command, args, environment: envVars, workingDirectory: cwd.isNotEmpty ? cwd : null, ); _activeCliProcess = process; await process.stdin.close(); // 超时控制 final timeout = Duration(seconds: _config.timeoutSeconds); final stdoutFuture = process.stdout .transform(utf8.decoder) .join() .timeout( timeout, onTimeout: () { process.kill(); return '[超时或进程已终止]'; }, ); final stderrFuture = process.stderr .transform(utf8.decoder) .join() .timeout(timeout, onTimeout: () => ''); final results = await Future.wait([stdoutFuture, stderrFuture]); final exitCode = await process.exitCode.timeout( timeout, onTimeout: () => -1, ); _activeCliProcess = null; final cliResult = CliResult( output: results[0], error: results[1], exitCode: exitCode, ); if (_config.usesAris && !cliResult.success) { return _runArisFallback( role: role, model: model, prompt: prompt, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); } return cliResult; } catch (e) { _activeCliProcess = null; if (_config.usesAris) { return _runArisFallback( role: role, model: model, prompt: prompt, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); } return CliResult(output: '', error: e.toString(), exitCode: -1); } } /// 构建 Architect 的 Prompt String _buildArchitectPrompt( String task, List selectedSkills, String instructionBlock, ) { return ''' $instructionBlock 你是一个多 Agent 协作调度者。请先收敛 requirements -> acceptance evidence,再输出可执行的主程/worker分工。 ## 用户需求 $task ## 优先技能 ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').join('\n')} 请输出: 1. 任务概述(2-3 句话) 2. 子任务列表(3-5 个),每个子任务包含: - 任务编号和描述 - 负责角色(文档/主程/worker) - 接受标准 - 关键技术点 3. 推荐的执行顺序与关键里程碑 请严格按以下格式输出: ## 概述 [你的概述] ## 子任务 1. [任务描述] | 角色:[文档/主程/worker] | 接受标准:[可验证结果] | 关键技术:[技术点] 2. [任务描述] | 角色:[文档/主程/worker] | 接受标准:[可验证结果] | 关键技术:[技术点] ... '''; } Future _resolveToolForRole( MultiAgentRole role, String configuredTool, ) async { if (!_config.usesAris) { return configuredTool; } final configuredModel = _resolvedModelForRole( role, configuredModel: _modelForRole(role).trim(), ); final candidates = switch (role) { MultiAgentRole.architect => [ configuredTool, 'claude', 'codex', 'opencode', 'gemini', ], MultiAgentRole.engineer => [ configuredTool, 'codex', 'opencode', 'claude', 'gemini', ], MultiAgentRole.testerDoc => [ configuredTool, 'opencode', 'codex', 'claude', 'gemini', ], }; for (final candidate in candidates) { final trimmed = candidate.trim(); if (trimmed.isEmpty) { continue; } if (_prefersOllamaLaunch(tool: trimmed, model: configuredModel)) { if (await _binaryExists('ollama')) { return trimmed; } } else if (await _binaryExists(_resolveCliPath(trimmed))) { return trimmed; } } return configuredTool; } String _resolvedModelForRole( MultiAgentRole role, { required String configuredModel, }) { final trimmed = configuredModel.trim(); if (trimmed.isNotEmpty) { return trimmed; } switch (role) { case MultiAgentRole.architect: return 'kimi-k2.5:cloud'; case MultiAgentRole.engineer: return 'minimax-m2.7:cloud'; case MultiAgentRole.testerDoc: return 'glm-5:cloud'; } } Future _binaryExists(String command) async { final resolver = _binaryExistsResolver; if (resolver != null) { return resolver(command); } final check = await Process.run( Platform.isWindows ? 'where' : 'which', [command], runInShell: true, ); return check.exitCode == 0 && '${check.stdout}'.trim().isNotEmpty; } Future _runArisFallback({ required MultiAgentRole role, required String model, required String prompt, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { if (role == MultiAgentRole.testerDoc) { final viaLlmChat = await _runArisTesterViaLlmChat( model: model, prompt: prompt, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); if (viaLlmChat.success) { return viaLlmChat; } } return _runOpenAiCompatiblePrompt( role: role, model: model, prompt: prompt, aiGatewayBaseUrl: aiGatewayBaseUrl, aiGatewayApiKey: aiGatewayApiKey, ); } Future _runArisTesterViaLlmChat({ required String model, required String prompt, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { try { if (!await _goCoreLocator.isAvailable()) { return const CliResult( output: '', error: 'Go core is unavailable for llm-chat', exitCode: -1, ); } final endpoint = _openAiCompatibleBaseUrl( aiGatewayBaseUrl: aiGatewayBaseUrl, ); final apiKey = _openAiCompatibleApiKey(aiGatewayApiKey: aiGatewayApiKey); final output = await _arisLlmChatClient.chat( endpoint: endpoint, apiKey: apiKey, model: model, prompt: prompt, systemPrompt: 'You are the ARIS reviewer. Review the provided implementation and return actionable feedback.', ); return CliResult(output: output, error: '', exitCode: 0); } catch (error) { return CliResult(output: '', error: error.toString(), exitCode: -1); } } Future _runArisTesterViaClaudeReview({ required String model, required String prompt, }) async { try { if (!await _goCoreLocator.isAvailable()) { return const CliResult( output: '', error: 'Go core is unavailable for claude-review', exitCode: -1, ); } if (!await _binaryExists(_resolveCliPath('claude'))) { return const CliResult( output: '', error: 'Claude CLI is unavailable for claude-review', exitCode: -1, ); } final output = await _arisLlmChatClient.claudeReview( prompt: prompt, model: model, systemPrompt: 'You are the ARIS reviewer. Review the provided implementation and return actionable feedback.', ); return CliResult(output: output, error: '', exitCode: 0); } catch (error) { return CliResult(output: '', error: error.toString(), exitCode: -1); } } Future _runOpenAiCompatiblePrompt({ required MultiAgentRole role, required String model, required String prompt, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) async { final client = _httpClientFactory(); _activeHttpClient = client; try { final request = await client.postUrl( Uri.parse( '${_openAiCompatibleBaseUrl(aiGatewayBaseUrl: aiGatewayBaseUrl).replaceAll(RegExp(r'/$'), '')}/chat/completions', ), ); request.headers.set(HttpHeaders.contentTypeHeader, 'application/json'); request.headers.set( HttpHeaders.authorizationHeader, 'Bearer ${_openAiCompatibleApiKey(aiGatewayApiKey: aiGatewayApiKey)}', ); request.add( utf8.encode( jsonEncode({ 'model': model, 'stream': false, 'messages': >[ { 'role': 'system', 'content': _systemPromptForRole(role), }, {'role': 'user', 'content': prompt}, ], }), ), ); final response = await request.close().timeout( Duration(seconds: _config.timeoutSeconds), ); final body = await utf8.decodeStream(response); if (response.statusCode < 200 || response.statusCode >= 300) { return CliResult( output: '', error: body, exitCode: response.statusCode, ); } final decoded = jsonDecode(body) as Map; final choices = decoded['choices'] as List? ?? const []; final firstChoice = choices.isNotEmpty ? choices.first : null; final output = ((firstChoice as Map?)?['message'] as Map?)?['content']?.toString() ?? ''; return CliResult(output: output, error: '', exitCode: 0); } catch (error) { return CliResult(output: '', error: error.toString(), exitCode: -1); } finally { _activeHttpClient = null; try { client.close(force: true); } catch (_) { // Best effort only. } } } String _openAiCompatibleBaseUrl({required String aiGatewayBaseUrl}) { if (_config.aiGatewayInjectionPolicy != AiGatewayInjectionPolicy.disabled && aiGatewayBaseUrl.trim().isNotEmpty) { final normalized = aiGatewayBaseUrl.trim(); return normalized.endsWith('/v1') ? normalized : '$normalized/v1'; } final normalized = _config.ollamaEndpoint.trim(); return normalized.endsWith('/v1') ? normalized : '$normalized/v1'; } String _openAiCompatibleApiKey({required String aiGatewayApiKey}) { if (_config.aiGatewayInjectionPolicy != AiGatewayInjectionPolicy.disabled && aiGatewayApiKey.trim().isNotEmpty) { return aiGatewayApiKey.trim(); } return 'ollama'; } String _systemPromptForRole(MultiAgentRole role) { return switch (role) { MultiAgentRole.architect => 'You are the architecture and documentation lane in a multi-agent coding workflow. Focus on requirements, acceptance evidence, task slicing, and milestones.', MultiAgentRole.engineer => 'You are the lead engineer in a multi-agent coding workflow. Produce implementation-oriented output for the critical path.', MultiAgentRole.testerDoc => 'You are the worker-review lane in a multi-agent coding workflow. Review, score, and suggest follow-up fixes and worker follow-ups.', }; } String _roleLabel(MultiAgentRole role) { return switch (role) { MultiAgentRole.architect => 'Architect', MultiAgentRole.engineer => 'Lead Engineer', MultiAgentRole.testerDoc => 'Worker/Review', }; } String _modelForRole(MultiAgentRole role) { return switch (role) { MultiAgentRole.architect => _config.architect.model, MultiAgentRole.engineer => _config.engineer.model, MultiAgentRole.testerDoc => _config.tester.model, }; } bool _prefersOllamaLaunch({required String tool, required String model}) { final normalizedTool = tool.trim().toLowerCase(); final normalizedModel = model.trim(); if (normalizedModel.isEmpty) { return false; } if (normalizedTool != 'claude' && normalizedTool != 'codex' && normalizedTool != 'opencode') { return false; } return true; } List _buildOllamaLaunchArgs({ required String tool, required String model, required String prompt, required String cwd, }) { final args = ['launch', tool, '--model', model]; if (tool == 'claude') { args.add('--yes'); args.addAll(['--', '-p', prompt]); return args; } if (tool == 'codex') { args.addAll([ '--', 'exec', '--skip-git-repo-check', '--color', 'never', if (cwd.isNotEmpty) ...['-C', cwd], prompt, ]); return args; } if (tool == 'opencode') { args.addAll([ '--', 'run', '--format', 'default', if (cwd.isNotEmpty) ...['--dir', cwd], prompt, ]); return args; } args.addAll(['--', '-p', prompt]); return args; } void _throwIfAborted() { if (_abortRequested) { throw StateError('Multi-agent collaboration aborted.'); } } /// 解析 Architect 分解的任务 List _parseDecomposedTasks(String architectOutput) { final tasks = []; final lines = architectOutput.split('\n'); var order = 1; for (final line in lines) { final trimmed = line.trim(); if (trimmed.isEmpty) continue; // 匹配 "- 描述" 或 "1. 描述" 格式 final dashMatch = RegExp(r'^[-*]\s+(.+)').firstMatch(trimmed); final numMatch = RegExp(r'^\d+[.、)]\s*(.+)').firstMatch(trimmed); String? description; if (dashMatch != null) { description = dashMatch.group(1); } else if (numMatch != null) { description = numMatch.group(1); } if (description != null && description.isNotEmpty) { // 去除复杂度等技术注释 description = description.replaceAll(RegExp(r'\s*\|.*'), '').trim(); // 判断任务类型 SubTaskType type = SubTaskType.implementation; final lower = description.toLowerCase(); if (lower.contains('测试') || lower.contains('test')) { type = SubTaskType.testing; } else if (lower.contains('文档') || lower.contains('doc')) { type = SubTaskType.documentation; } else if (lower.contains('设计') || lower.contains('design')) { type = SubTaskType.design; } else if (lower.contains('部署') || lower.contains('deploy')) { type = SubTaskType.deployment; } tasks.add( SubTask( id: order.toString(), description: description, order: order, type: type, ), ); order++; } } // 如果解析失败,至少返回一个包含完整需求的子任务 if (tasks.isEmpty) { tasks.add( SubTask( id: '1', description: architectOutput.length > 200 ? '${architectOutput.substring(0, 200)}...' : architectOutput, order: 1, type: SubTaskType.implementation, ), ); } return tasks; } /// 解析审阅评分 int _parseReviewScore(String output) { // 尝试匹配 "评分 (1-10)" 模式 final patterns = [ RegExp(r'评分\s*\(?[1100]\)?\s*[::]?\s*(\d+)'), RegExp(r'score\s*[::]?\s*(\d+)', caseSensitive: false), RegExp(r'评分[::\s]*(\d+)'), RegExp(r'\*\*(\d+)\s*/\s*10\*\*'), RegExp(r'(\d+)\s*/\s*10'), ]; for (final pattern in patterns) { final match = pattern.firstMatch(output); if (match != null) { final scoreStr = match.group(1)!; final score = int.tryParse( scoreStr.replaceAll('1', '1').replaceAll('0', '0'), ); if (score != null && score >= 1 && score <= 10) { return score; } } } // 默认中等评分 return 5; } /// 提取审阅反馈 String _extractFeedback(String output) { final feedbackIndex = output.indexOf(RegExp(r'##?\s*问题|##?\s*改进|##?\s*建议')); if (feedbackIndex >= 0) { final endIndex = output.indexOf( RegExp(r'##?\s*测试|##?\s*文档'), feedbackIndex + 1, ); if (endIndex > feedbackIndex) { return output.substring(feedbackIndex, endIndex).trim(); } return output.substring(feedbackIndex).trim(); } return output; } /// 构建 Ollama 环境变量 Map _buildCliEnvVars({ required String tool, required String aiGatewayBaseUrl, required String aiGatewayApiKey, }) { final baseEnv = {...Platform.environment}; if (_config.aiGatewayInjectionPolicy != AiGatewayInjectionPolicy.disabled && aiGatewayBaseUrl.trim().isNotEmpty && aiGatewayApiKey.trim().isNotEmpty) { baseEnv['OPENAI_BASE_URL'] = aiGatewayBaseUrl.trim(); baseEnv['OPENAI_API_KEY'] = aiGatewayApiKey.trim(); baseEnv['OLLAMA_BASE_URL'] = aiGatewayBaseUrl.trim(); baseEnv['OLLAMA_HOST'] = aiGatewayBaseUrl.trim(); if (tool == 'claude') { baseEnv['ANTHROPIC_BASE_URL'] = aiGatewayBaseUrl.trim(); baseEnv['ANTHROPIC_AUTH_TOKEN'] = aiGatewayApiKey.trim(); baseEnv['ANTHROPIC_API_KEY'] = aiGatewayApiKey.trim(); } return baseEnv; } final ollamaEndpoint = _config.ollamaEndpoint.trim(); if (ollamaEndpoint.isNotEmpty) { baseEnv['OLLAMA_BASE_URL'] = ollamaEndpoint; baseEnv['OLLAMA_HOST'] = ollamaEndpoint; baseEnv['OPENAI_API_KEY'] = 'ollama'; baseEnv['OPENAI_BASE_URL'] = ollamaEndpoint.endsWith('/v1') ? ollamaEndpoint : '$ollamaEndpoint/v1'; } if (tool == 'claude' || tool == 'codex') { baseEnv['ANTHROPIC_AUTH_TOKEN'] = 'ollama'; baseEnv['ANTHROPIC_API_KEY'] = ''; baseEnv['ANTHROPIC_BASE_URL'] = ollamaEndpoint; } return baseEnv; } /// 解析 CLI 工具路径 String _resolveCliPath(String tool) { switch (tool) { case 'claude': return 'claude'; case 'codex': return 'codex'; case 'gemini': return 'gemini'; case 'opencode': return 'opencode'; default: return tool; } } void _emitEvent( void Function(MultiAgentRunEvent event)? onEvent, MultiAgentRunEvent event, ) { onEvent?.call(event); } /// 记录日志 void _log(CollaborationLogLevel level, String emoji, String message) { _logEntries.add( CollaborationLogEntry( timestamp: DateTime.now(), level: level, emoji: emoji, message: message, ), ); notifyListeners(); } /// 清除日志 void clearLogs() { _logEntries.clear(); notifyListeners(); } } // ============================================================ // 数据模型 // ============================================================ /// 协作日志条目 class CollaborationLogEntry { const CollaborationLogEntry({ required this.timestamp, required this.level, required this.emoji, required this.message, }); final DateTime timestamp; final CollaborationLogLevel level; final String emoji; final String message; String get formattedTime { final h = timestamp.hour.toString().padLeft(2, '0'); final m = timestamp.minute.toString().padLeft(2, '0'); final s = timestamp.second.toString().padLeft(2, '0'); return '$h:$m:$s'; } } enum CollaborationLogLevel { debug, info, warning, error, success } /// CLI 执行结果 class CliResult { const CliResult({ required this.output, required this.error, required this.exitCode, }); final String output; final String error; final int exitCode; bool get success => exitCode == 0 && error.isEmpty; } /// Architect 执行结果 class ArchitectResult { ArchitectResult({ required this.output, required this.decomposedTasks, required this.duration, }); final String output; final List decomposedTasks; final Duration duration; } /// Engineer 执行结果 class EngineerResult { EngineerResult({ required this.output, required this.codeOutput, required this.completedTasks, required this.duration, }); final String output; String codeOutput; final List completedTasks; final Duration duration; } /// Tester 执行结果 class TesterResult { TesterResult({ required this.output, required this.score, required this.feedback, required this.duration, }); final String output; final int score; final String feedback; final Duration duration; } /// 协作步骤 class CollaborationStep { const CollaborationStep({ required this.role, required this.status, required this.output, required this.duration, this.iteration, this.score, }); final String role; final StepStatus status; final String output; final Duration duration; final int? iteration; final int? score; Map toJson() { return { 'role': role, 'status': status.name, 'output': output, 'durationMs': duration.inMilliseconds, if (iteration != null) 'iteration': iteration, if (score != null) 'score': score, }; } } enum StepStatus { pending, running, completed, failed } /// 子任务 class SubTask { const SubTask({ required this.id, required this.description, required this.order, required this.type, }); final String id; final String description; final int order; final SubTaskType type; } enum SubTaskType { design, implementation, testing, documentation, deployment } /// 附件 class CollaborationAttachment { const CollaborationAttachment({ required this.name, required this.description, required this.path, }); final String name; final String description; final String path; } /// 协作最终结果 class CollaborationResult { const CollaborationResult({ required this.success, required this.steps, required this.finalCode, required this.finalScore, required this.duration, required this.iterations, this.error, }); final bool success; final List steps; final String finalCode; final int finalScore; final Duration duration; final int iterations; final String? error; Map toJson() { return { 'success': success, 'steps': steps.map((item) => item.toJson()).toList(growable: false), 'finalCode': finalCode, 'finalScore': finalScore, 'durationMs': duration.inMilliseconds, 'iterations': iterations, if (error != null) 'error': error, }; } }