refactor(runtime): eliminate local agent process management and CLI probing
- Refactor MultiAgentOrchestrator to remove local process management. - Disable local CLI execution and existence checks in orchestration workflows. - Remove local process discovery and state reconciliation in mount adapters. - Mark local agent process launching as deprecated/disabled across runtime.
This commit is contained in:
parent
d3da1505f6
commit
c214594fa1
@ -302,49 +302,8 @@ class CodexRuntime extends ChangeNotifier {
|
||||
CodexAccount? get account => _account;
|
||||
Stream<CodexEvent> get events => _events.stream;
|
||||
|
||||
/// Find Codex binary in PATH or common locations.
|
||||
Future<String?> findCodexBinary() async {
|
||||
// Check environment variable first
|
||||
final envPath = Platform.environment['CODEX_PATH'];
|
||||
if (envPath != null && envPath.isNotEmpty) {
|
||||
final file = File(envPath);
|
||||
if (await file.exists()) {
|
||||
return envPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Try common locations
|
||||
final paths = defaultCodexBinaryCandidates();
|
||||
|
||||
for (final path in paths) {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find via platform-native lookup.
|
||||
try {
|
||||
final result = await Process.run(
|
||||
_lookupExecutableProgram(),
|
||||
_lookupExecutableArguments(),
|
||||
);
|
||||
if (result.exitCode == 0) {
|
||||
final lines = LineSplitter.split(
|
||||
result.stdout as String,
|
||||
).map((line) => line.trim()).where((line) => line.isNotEmpty);
|
||||
for (final path in lines) {
|
||||
if (await File(path).exists()) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
/// Find Codex binary (DEPRECATED: Use bridge instead).
|
||||
Future<String?> findCodexBinary() async => null;
|
||||
|
||||
/// Start Codex App Server in stdio mode (DEPRECATED: Use bridge instead).
|
||||
Future<void> startStdio({
|
||||
|
||||
@ -6,6 +6,9 @@ import 'multi_agent_mount_resolver.dart';
|
||||
import 'opencode_config_bridge.dart';
|
||||
import 'runtime_models.dart';
|
||||
|
||||
/// 协作模式挂载管理器
|
||||
///
|
||||
/// 在云中性设计下,挂载目标的发现与状态调和应通过桥接同步到远程端点。
|
||||
class MultiAgentMountManager {
|
||||
MultiAgentMountManager({
|
||||
CodexConfigBridge? codexConfigBridge,
|
||||
@ -62,14 +65,13 @@ class MultiAgentMountManager {
|
||||
}
|
||||
|
||||
Future<ArisMountProbe> _buildArisProbe() async {
|
||||
// ARIS is legacy and has been removed from assets.
|
||||
return const ArisMountProbe(
|
||||
available: false,
|
||||
bundleVersion: '',
|
||||
llmChatServerPath: '',
|
||||
skillCount: 0,
|
||||
bridgeAvailable: false,
|
||||
error: 'ARIS has been removed from application assets.',
|
||||
error: 'Legacy local agent execution is disabled.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,29 +81,12 @@ class MultiAgentMountManager {
|
||||
}) async {
|
||||
final states = <ManagedMountTargetState>[];
|
||||
for (final adapter in _adapters) {
|
||||
try {
|
||||
states.add(
|
||||
await adapter.reconcile(
|
||||
config: config,
|
||||
aiGatewayUrl: aiGatewayUrl,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
states.add(
|
||||
ManagedMountTargetState.placeholder(
|
||||
targetId: adapter.targetId,
|
||||
label: adapter.label,
|
||||
supportsSkills: adapter.supportsSkills,
|
||||
supportsMcp: adapter.supportsMcp,
|
||||
supportsAiGatewayInjection: adapter.supportsAiGatewayInjection,
|
||||
).copyWith(
|
||||
available: await adapter.isInstalled(),
|
||||
discoveryState: 'error',
|
||||
syncState: 'error',
|
||||
detail: error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
states.add(
|
||||
await adapter.reconcile(
|
||||
config: config,
|
||||
aiGatewayUrl: aiGatewayUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
return config.copyWith(
|
||||
mountTargets: states,
|
||||
@ -125,36 +110,6 @@ abstract class CliMountAdapter {
|
||||
required String aiGatewayUrl,
|
||||
});
|
||||
|
||||
Future<String> _runCommand(List<String> command) async {
|
||||
final result = await Process.run(
|
||||
command.first,
|
||||
command.sublist(1),
|
||||
runInShell: true,
|
||||
);
|
||||
final stdout = '${result.stdout}'.trim();
|
||||
final stderr = '${result.stderr}'.trim();
|
||||
return stdout.isNotEmpty ? stdout : stderr;
|
||||
}
|
||||
|
||||
Future<int> _countListedEntries(List<String> command) async {
|
||||
final output = await _runCommand(command);
|
||||
if (output.isEmpty ||
|
||||
output.contains('No MCP servers configured') ||
|
||||
output.contains('No MCP servers configured yet') ||
|
||||
output.contains('No MCP servers configured.')) {
|
||||
return 0;
|
||||
}
|
||||
return output
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.where((item) => !item.startsWith('Usage:'))
|
||||
.where((item) => !item.startsWith('┌'))
|
||||
.where((item) => !item.startsWith('│'))
|
||||
.where((item) => !item.startsWith('└'))
|
||||
.length;
|
||||
}
|
||||
|
||||
int countMcpTomlSections(String content) {
|
||||
return RegExp(
|
||||
r'^\[mcp_servers\.[^\]]+\]',
|
||||
@ -191,28 +146,6 @@ class CodexMountAdapter extends CliMountAdapter {
|
||||
required MultiAgentConfig config,
|
||||
required String aiGatewayUrl,
|
||||
}) async {
|
||||
final available = await isInstalled();
|
||||
final configFile = File('${_bridge.codexHome}/config.toml');
|
||||
final content = await configFile.exists()
|
||||
? await configFile.readAsString()
|
||||
: '';
|
||||
final discoveredMcpCount = countMcpTomlSections(content);
|
||||
final managedMcpServers = config.managedMcpServers
|
||||
.where((item) => item.enabled && item.command.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (available && config.autoSync && managedMcpServers.isNotEmpty) {
|
||||
await _bridge.configureManagedMcpServers(
|
||||
servers: managedMcpServers
|
||||
.map(
|
||||
(item) => CodexMcpServer(
|
||||
name: item.id,
|
||||
command: item.command,
|
||||
args: item.args,
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
return ManagedMountTargetState.placeholder(
|
||||
targetId: targetId,
|
||||
label: label,
|
||||
@ -220,18 +153,10 @@ class CodexMountAdapter extends CliMountAdapter {
|
||||
supportsMcp: supportsMcp,
|
||||
supportsAiGatewayInjection: supportsAiGatewayInjection,
|
||||
).copyWith(
|
||||
available: available,
|
||||
discoveryState: available ? 'ready' : 'missing',
|
||||
syncState: !available
|
||||
? 'missing'
|
||||
: config.autoSync
|
||||
? 'ready'
|
||||
: 'disabled',
|
||||
discoveredMcpCount: discoveredMcpCount,
|
||||
managedMcpCount: managedMcpServers.length,
|
||||
detail: aiGatewayUrl.isNotEmpty
|
||||
? 'LLM API uses launch-scoped defaults for collaboration runs.'
|
||||
: 'LLM API not configured.',
|
||||
available: false,
|
||||
discoveryState: 'missing',
|
||||
syncState: 'missing',
|
||||
detail: 'Local CLI interaction is disabled. Use bridge for orchestration.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -260,10 +185,6 @@ class ClaudeMountAdapter extends CliMountAdapter {
|
||||
required MultiAgentConfig config,
|
||||
required String aiGatewayUrl,
|
||||
}) async {
|
||||
final available = await isInstalled();
|
||||
final discoveredMcpCount = available
|
||||
? await _countListedEntries(<String>['claude', 'mcp', 'list'])
|
||||
: 0;
|
||||
return ManagedMountTargetState.placeholder(
|
||||
targetId: targetId,
|
||||
label: label,
|
||||
@ -271,15 +192,10 @@ class ClaudeMountAdapter extends CliMountAdapter {
|
||||
supportsMcp: supportsMcp,
|
||||
supportsAiGatewayInjection: supportsAiGatewayInjection,
|
||||
).copyWith(
|
||||
available: available,
|
||||
discoveryState: available ? 'ready' : 'missing',
|
||||
syncState: available && config.autoSync ? 'launch-only' : 'disabled',
|
||||
discoveredMcpCount: discoveredMcpCount,
|
||||
managedMcpCount: config.managedMcpServers
|
||||
.where((item) => item.enabled)
|
||||
.length,
|
||||
detail:
|
||||
'MCP discovery uses `claude mcp list`; LLM API stays launch-scoped.',
|
||||
available: false,
|
||||
discoveryState: 'missing',
|
||||
syncState: 'disabled',
|
||||
detail: 'Local CLI interaction is disabled.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -308,10 +224,6 @@ class GeminiMountAdapter extends CliMountAdapter {
|
||||
required MultiAgentConfig config,
|
||||
required String aiGatewayUrl,
|
||||
}) async {
|
||||
final available = await isInstalled();
|
||||
final discoveredMcpCount = available
|
||||
? await _countListedEntries(<String>['gemini', 'mcp', 'list'])
|
||||
: 0;
|
||||
return ManagedMountTargetState.placeholder(
|
||||
targetId: targetId,
|
||||
label: label,
|
||||
@ -319,15 +231,10 @@ class GeminiMountAdapter extends CliMountAdapter {
|
||||
supportsMcp: supportsMcp,
|
||||
supportsAiGatewayInjection: supportsAiGatewayInjection,
|
||||
).copyWith(
|
||||
available: available,
|
||||
discoveryState: available ? 'ready' : 'missing',
|
||||
syncState: available && config.autoSync ? 'launch-only' : 'disabled',
|
||||
discoveredMcpCount: discoveredMcpCount,
|
||||
managedMcpCount: config.managedMcpServers
|
||||
.where((item) => item.enabled)
|
||||
.length,
|
||||
detail:
|
||||
'MCP discovery uses `gemini mcp list`; LLM API stays launch-scoped.',
|
||||
available: false,
|
||||
discoveryState: 'missing',
|
||||
syncState: 'disabled',
|
||||
detail: 'Local CLI interaction is disabled.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -360,26 +267,6 @@ class OpencodeMountAdapter extends CliMountAdapter {
|
||||
required MultiAgentConfig config,
|
||||
required String aiGatewayUrl,
|
||||
}) async {
|
||||
final available = await isInstalled();
|
||||
final content = await _bridge.readConfig();
|
||||
final discoveredMcpCount = countMcpTomlSections(content);
|
||||
final managedMcpServers = config.managedMcpServers
|
||||
.where((item) => item.enabled)
|
||||
.toList(growable: false);
|
||||
if (available && config.autoSync && managedMcpServers.isNotEmpty) {
|
||||
await _bridge.configureManagedMcpServers(
|
||||
servers: managedMcpServers
|
||||
.map(
|
||||
(item) => OpencodeMcpServer(
|
||||
name: item.id,
|
||||
command: item.command,
|
||||
url: item.url,
|
||||
args: item.args,
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
return ManagedMountTargetState.placeholder(
|
||||
targetId: targetId,
|
||||
label: label,
|
||||
@ -387,16 +274,10 @@ class OpencodeMountAdapter extends CliMountAdapter {
|
||||
supportsMcp: supportsMcp,
|
||||
supportsAiGatewayInjection: supportsAiGatewayInjection,
|
||||
).copyWith(
|
||||
available: available,
|
||||
discoveryState: available ? 'ready' : 'missing',
|
||||
syncState: !available
|
||||
? 'missing'
|
||||
: config.autoSync
|
||||
? 'ready'
|
||||
: 'disabled',
|
||||
discoveredMcpCount: discoveredMcpCount,
|
||||
managedMcpCount: managedMcpServers.length,
|
||||
detail: 'Managed MCP config is preserved in ~/.opencode/config.toml.',
|
||||
available: false,
|
||||
discoveryState: 'missing',
|
||||
syncState: 'missing',
|
||||
detail: 'Local CLI interaction is disabled.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -425,36 +306,6 @@ class OpenClawMountAdapter extends CliMountAdapter {
|
||||
required MultiAgentConfig config,
|
||||
required String aiGatewayUrl,
|
||||
}) async {
|
||||
final available = await isInstalled();
|
||||
final configFile = File(
|
||||
'${Platform.environment['HOME'] ?? ''}/.openclaw/openclaw.json',
|
||||
);
|
||||
var discoveredSkillCount = 0;
|
||||
var detail = 'OpenClaw acts as the host/control plane mount.';
|
||||
if (await configFile.exists()) {
|
||||
try {
|
||||
final decoded = jsonDecode(await configFile.readAsString());
|
||||
final agents =
|
||||
(decoded is Map<String, dynamic> &&
|
||||
decoded['agents'] is Map<String, dynamic> &&
|
||||
(decoded['agents'] as Map<String, dynamic>)['list'] is List)
|
||||
? ((decoded['agents'] as Map<String, dynamic>)['list'] as List)
|
||||
.length
|
||||
: 0;
|
||||
final skillsDir = Directory(
|
||||
'${Platform.environment['HOME'] ?? ''}/.openclaw/skills',
|
||||
);
|
||||
if (await skillsDir.exists()) {
|
||||
discoveredSkillCount = await skillsDir
|
||||
.list()
|
||||
.where((entity) => entity is File || entity is Directory)
|
||||
.length;
|
||||
}
|
||||
detail = 'agents: $agents · skills: $discoveredSkillCount';
|
||||
} catch (_) {
|
||||
detail = 'OpenClaw config detected but could not be fully parsed.';
|
||||
}
|
||||
}
|
||||
return ManagedMountTargetState.placeholder(
|
||||
targetId: targetId,
|
||||
label: label,
|
||||
@ -462,11 +313,10 @@ class OpenClawMountAdapter extends CliMountAdapter {
|
||||
supportsMcp: supportsMcp,
|
||||
supportsAiGatewayInjection: supportsAiGatewayInjection,
|
||||
).copyWith(
|
||||
available: available,
|
||||
discoveryState: available ? 'ready' : 'missing',
|
||||
syncState: available && config.autoSync ? 'launch-only' : 'disabled',
|
||||
discoveredSkillCount: discoveredSkillCount,
|
||||
detail: detail,
|
||||
available: false,
|
||||
discoveryState: 'missing',
|
||||
syncState: 'disabled',
|
||||
detail: 'Local CLI interaction is disabled.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,39 +14,21 @@ import 'multi_agent_orchestrator_support.dart';
|
||||
/// 多 Agent 协作编排器
|
||||
///
|
||||
/// 管理 Architect(调度/文档)→ Lead Engineer(主程)→ Worker/Review(并行 worker + 复审)
|
||||
/// 的工作流,通过 Ollama 与外部 CLI 工具桥接首批云模型协作能力。
|
||||
/// 的工作流。
|
||||
///
|
||||
/// 角色分工:
|
||||
/// - Architect(调度/文档):负责任务分解、接受标准、工作流设计
|
||||
/// - Lead Engineer(主程):负责关键实现、重构、集成收口
|
||||
/// - Worker/Review(并行 worker):负责补充实现、复审、回归建议
|
||||
/// 在云中性设计下,编排逻辑应通过桥接转发到远程 ACP 端点执行。
|
||||
class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
MultiAgentOrchestrator({
|
||||
required MultiAgentConfig config,
|
||||
Future<bool> Function(String command)? binaryExistsResolver,
|
||||
HttpClient Function()? httpClientFactory,
|
||||
CliProcessStarter? processStarter,
|
||||
}) : configInternal = config,
|
||||
binaryExistsResolverInternal = binaryExistsResolver,
|
||||
httpClientFactoryInternal = httpClientFactory ?? HttpClient.new,
|
||||
processStarterInternal =
|
||||
processStarter ??
|
||||
((executable, arguments, {environment, workingDirectory}) {
|
||||
return Process.start(
|
||||
executable,
|
||||
arguments,
|
||||
environment: environment,
|
||||
workingDirectory: workingDirectory,
|
||||
);
|
||||
});
|
||||
httpClientFactoryInternal = httpClientFactory ?? HttpClient.new;
|
||||
|
||||
/// 当前配置
|
||||
MultiAgentConfig configInternal;
|
||||
MultiAgentConfig get config => configInternal;
|
||||
final Future<bool> Function(String command)? binaryExistsResolverInternal;
|
||||
final HttpClient Function() httpClientFactoryInternal;
|
||||
final CliProcessStarter processStarterInternal;
|
||||
Process? activeCliProcessInternal;
|
||||
|
||||
HttpClient? activeHttpClientInternal;
|
||||
bool abortRequestedInternal = false;
|
||||
|
||||
@ -80,15 +62,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
|
||||
Future<void> abort() async {
|
||||
abortRequestedInternal = true;
|
||||
final process = activeCliProcessInternal;
|
||||
activeCliProcessInternal = null;
|
||||
if (process != null) {
|
||||
try {
|
||||
process.kill();
|
||||
} catch (_) {
|
||||
// Best effort only.
|
||||
}
|
||||
}
|
||||
final client = activeHttpClientInternal;
|
||||
activeHttpClientInternal = null;
|
||||
if (client != null) {
|
||||
@ -100,16 +73,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void assertEmbeddedProcessesAllowedInternal() {
|
||||
if (shouldBlockEmbeddedAgentLaunch(
|
||||
isAppleHost: Platform.isIOS || Platform.isMacOS,
|
||||
)) {
|
||||
throw UnsupportedError(
|
||||
'App Store builds do not allow launching embedded multi-agent subprocesses.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 启用协作模式
|
||||
void enable() {
|
||||
configInternal = configInternal.copyWith(enabled: true);
|
||||
@ -135,8 +98,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 执行完整的协作工作流
|
||||
///
|
||||
/// 流程:Architect 分析 → Engineer 实现 → Tester 审阅 → 迭代(如需要)
|
||||
Future<CollaborationResult> runCollaboration({
|
||||
required String taskPrompt,
|
||||
required String workingDirectory,
|
||||
@ -144,7 +105,6 @@ class MultiAgentOrchestrator extends ChangeNotifier {
|
||||
List<String> selectedSkills = const [],
|
||||
void Function(MultiAgentRunEvent event)? onEvent,
|
||||
}) async {
|
||||
assertEmbeddedProcessesAllowedInternal();
|
||||
if (isRunningInternal) {
|
||||
throw StateError('Collaboration is already running');
|
||||
}
|
||||
|
||||
@ -269,7 +269,7 @@ $originalCode
|
||||
);
|
||||
}
|
||||
|
||||
/// 通用的 CLI 进程执行方法
|
||||
/// 通用的 CLI 进程执行方法 (DEPRECATED: Use bridge instead)
|
||||
Future<CliResult> runCliPromptInternal({
|
||||
required MultiAgentRole role,
|
||||
required String tool,
|
||||
@ -277,167 +277,9 @@ $originalCode
|
||||
required String prompt,
|
||||
required String cwd,
|
||||
}) async {
|
||||
late final List<String> args;
|
||||
late final String command;
|
||||
late final Map<String, String> envVars;
|
||||
final useOllamaLaunch = prefersOllamaLaunchInternal(
|
||||
tool: tool,
|
||||
model: model,
|
||||
);
|
||||
|
||||
switch (tool) {
|
||||
case 'claude':
|
||||
command = useOllamaLaunch ? 'ollama' : resolveCliPathInternal('claude');
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
if (useOllamaLaunch) {
|
||||
args = buildOllamaLaunchArgsInternal(
|
||||
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' : resolveCliPathInternal('codex');
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
if (useOllamaLaunch) {
|
||||
args = buildOllamaLaunchArgsInternal(
|
||||
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 = resolveCliPathInternal('gemini');
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
if (model.isNotEmpty) {
|
||||
args = ['--model', model, '-p', prompt];
|
||||
} else {
|
||||
args = ['-p', prompt];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'opencode':
|
||||
command = useOllamaLaunch
|
||||
? 'ollama'
|
||||
: resolveCliPathInternal('opencode');
|
||||
envVars = buildCliEnvVarsInternal(tool: tool);
|
||||
args = useOllamaLaunch
|
||||
? buildOllamaLaunchArgsInternal(
|
||||
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 binaryExistsInternal(command);
|
||||
if (configInternal.usesAris && !cliAvailable) {
|
||||
return runArisFallbackInternal(role: role, model: model, prompt: prompt);
|
||||
}
|
||||
|
||||
try {
|
||||
final process = await processStarterInternal(
|
||||
command,
|
||||
args,
|
||||
environment: envVars,
|
||||
workingDirectory: cwd.isNotEmpty ? cwd : null,
|
||||
);
|
||||
activeCliProcessInternal = process;
|
||||
|
||||
await process.stdin.close();
|
||||
|
||||
// 超时控制
|
||||
final timeout = Duration(seconds: configInternal.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,
|
||||
);
|
||||
activeCliProcessInternal = null;
|
||||
|
||||
final cliResult = CliResult(
|
||||
output: results[0],
|
||||
error: results[1],
|
||||
exitCode: exitCode,
|
||||
);
|
||||
if (configInternal.usesAris && !cliResult.success) {
|
||||
return runArisFallbackInternal(
|
||||
role: role,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
);
|
||||
}
|
||||
return cliResult;
|
||||
} catch (e) {
|
||||
activeCliProcessInternal = null;
|
||||
if (configInternal.usesAris) {
|
||||
return runArisFallbackInternal(
|
||||
role: role,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
);
|
||||
}
|
||||
return CliResult(output: '', error: e.toString(), exitCode: -1);
|
||||
}
|
||||
// In cloud-neutral architecture, local CLI execution is disabled.
|
||||
// We should fallback to OpenAI compatible API or bridge execution.
|
||||
return runArisFallbackInternal(role: role, model: model, prompt: prompt);
|
||||
}
|
||||
|
||||
/// 构建 Architect 的 Prompt
|
||||
@ -481,49 +323,6 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
MultiAgentRole role,
|
||||
String configuredTool,
|
||||
) async {
|
||||
if (!configInternal.usesAris) {
|
||||
return configuredTool;
|
||||
}
|
||||
final configuredModel = resolvedModelForRoleInternal(
|
||||
role,
|
||||
configuredModel: modelForRoleInternal(role).trim(),
|
||||
);
|
||||
final candidates = switch (role) {
|
||||
MultiAgentRole.architect => <String>[
|
||||
configuredTool,
|
||||
'claude',
|
||||
'codex',
|
||||
'opencode',
|
||||
'gemini',
|
||||
],
|
||||
MultiAgentRole.engineer => <String>[
|
||||
configuredTool,
|
||||
'codex',
|
||||
'opencode',
|
||||
'claude',
|
||||
'gemini',
|
||||
],
|
||||
MultiAgentRole.testerDoc => <String>[
|
||||
configuredTool,
|
||||
'opencode',
|
||||
'codex',
|
||||
'claude',
|
||||
'gemini',
|
||||
],
|
||||
};
|
||||
for (final candidate in candidates) {
|
||||
final trimmed = candidate.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (prefersOllamaLaunchInternal(tool: trimmed, model: configuredModel)) {
|
||||
if (await binaryExistsInternal('ollama')) {
|
||||
return trimmed;
|
||||
}
|
||||
} else if (await binaryExistsInternal(resolveCliPathInternal(trimmed))) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return configuredTool;
|
||||
}
|
||||
|
||||
@ -546,16 +345,7 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
}
|
||||
|
||||
Future<bool> binaryExistsInternal(String command) async {
|
||||
final resolver = binaryExistsResolverInternal;
|
||||
if (resolver != null) {
|
||||
return resolver(command);
|
||||
}
|
||||
final check = await Process.run(
|
||||
Platform.isWindows ? 'where' : 'which',
|
||||
<String>[command],
|
||||
runInShell: true,
|
||||
);
|
||||
return check.exitCode == 0 && '${check.stdout}'.trim().isNotEmpty;
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<CliResult> runArisFallbackInternal({
|
||||
@ -594,19 +384,10 @@ ${selectedSkills.isEmpty ? '- 无' : selectedSkills.map((item) => '- $item').joi
|
||||
required String model,
|
||||
required String prompt,
|
||||
}) async {
|
||||
if (await binaryExistsInternal(resolveCliPathInternal('claude'))) {
|
||||
return runCliPromptInternal(
|
||||
role: MultiAgentRole.testerDoc,
|
||||
tool: 'claude',
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
cwd: '',
|
||||
);
|
||||
}
|
||||
return CliResult(
|
||||
output: '',
|
||||
error: 'Claude CLI is unavailable for claude-review',
|
||||
exitCode: -1,
|
||||
return runArisFallbackInternal(
|
||||
role: MultiAgentRole.testerDoc,
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user