xworkmate-app/lib/features/settings/settings_page_multi_agent.dart

646 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/app_store_policy.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_navigation.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/gateway_runtime.dart';
import '../../runtime/runtime_controllers.dart';
import '../../runtime/runtime_models.dart';
import 'codex_integration_card.dart';
import 'skill_directory_authorization_card.dart';
import '../../widgets/section_tabs.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
import 'settings_page_core.dart';
import 'settings_page_sections.dart';
import 'settings_page_gateway.dart';
import 'settings_page_gateway_connection.dart';
import 'settings_page_gateway_llm.dart';
import 'settings_page_presentation.dart';
import 'settings_page_support.dart';
import 'settings_page_device.dart';
import 'settings_page_widgets.dart';
extension SettingsPageMultiAgentMixinInternal on SettingsPageStateInternal {
List<Widget> buildAgentsInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
) {
final orchestrator = controller.multiAgentOrchestrator;
final config = settings.multiAgent;
final theme = Theme.of(context);
final mountTargets = List<ManagedMountTargetState>.from(config.mountTargets)
..sort(
(left, right) =>
left.label.toLowerCase().compareTo(right.label.toLowerCase()),
);
final managedSkillCount = config.managedSkills
.where((item) => item.selected)
.length;
final managedMcpCount = config.managedMcpServers
.where((item) => item.enabled)
.length;
return [
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LayoutBuilder(
builder: (context, constraints) {
final compact = constraints.maxWidth < 760;
final info = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('多 Agent 协作', 'Multi-Agent Collaboration'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
appText(
'限定在多 Agent 协作Architect 负责调度/文档Lead Engineer 负责主程Worker/Review 负责并行 worker 与复审;第一批外部桥接走 ollama launch。',
'Multi-agent only: Architect handles orchestration/docs, Lead Engineer owns the critical path, Worker/Review handles parallel workers and review; first-batch external bridges run through ollama launch.',
),
style: theme.textTheme.bodyMedium,
),
],
);
final toggle = InlineSwitchFieldInternal(
label: appText('启用协作模式', 'Enable Collaboration'),
value: config.enabled,
onChanged: (value) => saveMultiAgentConfigInternal(
controller,
config.copyWith(enabled: value),
),
);
if (compact) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [info, const SizedBox(height: 16), toggle],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: info),
const SizedBox(width: 20),
Flexible(
child: Align(
alignment: Alignment.topRight,
child: toggle,
),
),
],
);
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
key: ValueKey('multi-agent-framework-${config.framework.name}'),
initialValue: config.framework.name,
decoration: InputDecoration(
labelText: appText('协作框架', 'Framework'),
),
items: MultiAgentFramework.values
.map(
(framework) => DropdownMenuItem<String>(
value: framework.name,
child: Text(framework.label),
),
)
.toList(growable: false),
onChanged: (value) {
if (value == null) {
return;
}
final framework = MultiAgentFrameworkCopy.fromJsonValue(value);
saveMultiAgentConfigInternal(
controller,
config.copyWith(
framework: framework,
arisEnabled: framework == MultiAgentFramework.aris,
),
);
},
),
const SizedBox(height: 12),
InfoRowInternal(label: 'Ollama', value: config.ollamaEndpoint),
InfoRowInternal(
label: appText('文档 Lane', 'Doc Lane'),
value:
'${config.architect.cliTool} · ${config.architect.model.isEmpty ? '' : config.architect.model}',
),
InfoRowInternal(
label: appText('主程 Lane', 'Lead Lane'),
value:
'${config.engineer.cliTool} · ${config.engineer.model.isEmpty ? '' : config.engineer.model}',
),
InfoRowInternal(
label: appText('Worker Lane', 'Worker Lane'),
value:
'${config.tester.cliTool} · ${config.tester.model.isEmpty ? '' : config.tester.model}',
),
InfoRowInternal(
label: appText('超时时间', 'Timeout'),
value: '${config.timeoutSeconds}s',
),
InfoRowInternal(
label: 'ARIS',
value: config.usesAris
? [
config.arisCompatStatus,
if (config.arisBundleVersion.trim().isNotEmpty)
config.arisBundleVersion.trim(),
].join(' · ')
: appText('未启用', 'Disabled'),
),
InfoRowInternal(
label: appText('运行状态', 'Runtime'),
value: orchestrator.isRunning
? appText('协作执行中', 'Collaboration running')
: config.enabled
? appText('已启用', 'Enabled')
: appText('已停用', 'Disabled'),
),
],
),
),
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('角色配置', 'Role Configuration'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 16),
AgentRoleCardInternal(
title:
'🧭 ${appText('Architect调度/文档)', 'Architect (Docs / Scheduler)')}',
description: appText(
'负责 requirements -> acceptance evidence、架构选项排序、文档与调度。',
'Owns requirements -> acceptance evidence, option ranking, docs, and orchestration.',
),
cliTool: config.architect.cliTool,
model: config.architect.model,
enabled: config.architect.enabled,
cliOptions: mergeOptionsInternal(config.architect.cliTool, const [
'claude',
'codex',
'opencode',
'gemini',
]),
modelOptions: getArchitectModelOptionsInternal(settings, config),
onCliChanged: (tool) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
architect: config.architect.copyWith(cliTool: tool),
),
),
onModelChanged: (model) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
architect: config.architect.copyWith(model: model),
),
),
onEnabledChanged: (enabled) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
architect: config.architect.copyWith(enabled: enabled),
),
),
),
const SizedBox(height: 12),
AgentRoleCardInternal(
title: '🔧 ${appText('Lead Engineer主程', 'Lead Engineer')}',
description: appText(
'负责关键实现、重构、集成收口,默认走 codex + minimax-m2.7:cloud。',
'Owns critical implementation, refactors, and integration. Defaults to codex + minimax-m2.7:cloud.',
),
cliTool: config.engineer.cliTool,
model: config.engineer.model,
enabled: config.engineer.enabled,
cliOptions: mergeOptionsInternal(config.engineer.cliTool, const [
'codex',
'claude',
'opencode',
'gemini',
]),
modelOptions: getLeadModelOptionsInternal(settings, config),
onCliChanged: (tool) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
engineer: config.engineer.copyWith(cliTool: tool),
),
),
onModelChanged: (model) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
engineer: config.engineer.copyWith(model: model),
),
),
onEnabledChanged: (enabled) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
engineer: config.engineer.copyWith(enabled: enabled),
),
),
),
const SizedBox(height: 12),
AgentRoleCardInternal(
title:
'🧪 ${appText('Worker/ReviewWorker 池)', 'Worker/Review Pool')}',
description: appText(
'负责 glm/qwen worker lane、回归审阅和补充建议。',
'Owns glm/qwen worker lanes, review, regression checks, and follow-up notes.',
),
cliTool: config.tester.cliTool,
model: config.tester.model,
enabled: config.tester.enabled,
cliOptions: mergeOptionsInternal(config.tester.cliTool, const [
'opencode',
'codex',
'claude',
'gemini',
]),
modelOptions: getWorkerModelOptionsInternal(settings, config),
onCliChanged: (tool) => saveMultiAgentConfigInternal(
controller,
config.copyWith(tester: config.tester.copyWith(cliTool: tool)),
),
onModelChanged: (model) => saveMultiAgentConfigInternal(
controller,
config.copyWith(tester: config.tester.copyWith(model: model)),
),
onEnabledChanged: (enabled) => saveMultiAgentConfigInternal(
controller,
config.copyWith(
tester: config.tester.copyWith(enabled: enabled),
),
),
),
],
),
),
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('审阅策略', 'Review Strategy'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: EditableFieldInternal(
label: appText('最大迭代次数', 'Max Iterations'),
value: config.maxIterations.toString(),
onSubmitted: (value) {
final parsed = int.tryParse(value.trim());
if (parsed != null && parsed > 0) {
saveMultiAgentConfigInternal(
controller,
config.copyWith(maxIterations: parsed),
);
}
},
),
),
const SizedBox(width: 16),
Expanded(
child: EditableFieldInternal(
label: appText('最低达标分数', 'Min Acceptable Score'),
value: config.minAcceptableScore.toString(),
onSubmitted: (value) {
final parsed = int.tryParse(value.trim());
if (parsed != null && parsed >= 1 && parsed <= 10) {
saveMultiAgentConfigInternal(
controller,
config.copyWith(minAcceptableScore: parsed),
);
}
},
),
),
],
),
const SizedBox(height: 8),
Text(
appText(
'当 Worker/Review 评分低于最低分数时,将进入迭代审阅循环。最多迭代指定次数。',
'When the Worker/Review score is below minimum, the iteration loop runs until max iterations or the score passes.',
),
style: theme.textTheme.bodySmall,
),
],
),
),
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LayoutBuilder(
builder: (context, constraints) {
final compact = constraints.maxWidth < 760;
final info = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('发现与分发', 'Discovery & Distribution'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
appText(
'App 作为统一发现与分发中心,维护托管 skills、MCP server list 和 LLM API 默认注入,但不会覆盖用户原有 CLI 配置。',
'The app acts as the discovery and distribution center for managed skills, MCP server lists, and LLM API defaults without overwriting existing CLI config.',
),
style: theme.textTheme.bodyMedium,
),
],
);
final refreshButton = OutlinedButton(
onPressed: () =>
controller.refreshMultiAgentMounts(sync: config.autoSync),
child: Text(appText('刷新挂载', 'Refresh Mounts')),
);
if (compact) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [info, const SizedBox(height: 12), refreshButton],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: info),
const SizedBox(width: 16),
refreshButton,
],
);
},
),
const SizedBox(height: 16),
SwitchRowInternal(
label: appText('自动同步托管配置', 'Auto-sync managed config'),
value: config.autoSync,
onChanged: (value) => saveMultiAgentConfigInternal(
controller,
config.copyWith(autoSync: value),
),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
key: ValueKey(
'multi-agent-injection-${config.aiGatewayInjectionPolicy.name}',
),
initialValue: config.aiGatewayInjectionPolicy.name,
decoration: InputDecoration(
labelText: appText('LLM API 注入策略', 'LLM API Injection'),
),
items: AiGatewayInjectionPolicy.values
.map(
(policy) => DropdownMenuItem<String>(
value: policy.name,
child: Text(policy.label),
),
)
.toList(growable: false),
onChanged: (value) {
if (value == null) {
return;
}
saveMultiAgentConfigInternal(
controller,
config.copyWith(
aiGatewayInjectionPolicy:
AiGatewayInjectionPolicyCopy.fromJsonValue(value),
),
);
},
),
const SizedBox(height: 16),
InfoRowInternal(
label: appText('托管 Skills', 'Managed Skills'),
value: '$managedSkillCount',
),
InfoRowInternal(
label: appText('托管 MCP', 'Managed MCP'),
value: '$managedMcpCount',
),
if (config.usesAris) ...[
const SizedBox(height: 4),
Text(
appText(
'ARIS 模式会把内嵌 skills 与 Go core reviewer 作为本地 Ollama 协作增强层,不会覆盖你原有的 CLI 全局配置。',
'ARIS mode injects embedded skills and the Go core reviewer for local Ollama collaboration without overwriting your existing CLI global config.',
),
style: theme.textTheme.bodySmall,
),
],
const SizedBox(height: 16),
...mountTargets.map(
(target) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: MountTargetCardInternal(target: target),
),
),
],
),
),
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('协作流程概览', 'Workflow Overview'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 12),
WorkflowStepInternal(
label: '1',
emoji: '🧭',
title: appText(
'Architect调度/文档)',
'Architect (Docs / Scheduler)',
),
desc: appText(
'收敛 requirements -> acceptance evidence并冻结里程碑。',
'Freeze requirements -> acceptance evidence and milestones.',
),
),
WorkflowStepInternal(
label: '2',
emoji: '🔧',
title: appText('Lead Engineer主程', 'Lead Engineer'),
desc: appText(
'主程执行关键路径与集成收口。',
'Lead engineer executes the critical path and integration.',
),
),
WorkflowStepInternal(
label: '3',
emoji: '🧪',
title: appText('Worker/ReviewWorker 池)', 'Worker/Review Pool'),
desc: appText(
'并行 worker 补切片review lane 给出复审与回归建议。',
'Parallel workers handle bounded slices while the review lane returns critique and regression guidance.',
),
),
WorkflowStepInternal(
label: '',
emoji: '🔄',
title: appText('迭代(如需要)', 'Iterate (if needed)'),
desc: appText(
'主程修复 -> Worker/Review 重新审阅',
'Lead engineer fixes -> Worker/Review re-reviews',
),
),
const SizedBox(height: 8),
Text(
appText(
'首批支持的外部启动模式:`ollama launch claude --model kimi-k2.5:cloud --yes -- -p ...`、`ollama launch codex --model minimax-m2.7:cloud -- exec ...`、`ollama launch opencode --model glm-5:cloud -- run ...`。',
'First-batch launch bridges: `ollama launch claude --model kimi-k2.5:cloud --yes -- -p ...`, `ollama launch codex --model minimax-m2.7:cloud -- exec ...`, and `ollama launch opencode --model glm-5:cloud -- run ...`.',
),
style: theme.textTheme.bodySmall,
),
],
),
),
];
}
List<String> getLocalModelOptionsInternal(SettingsSnapshot settings) {
return <String>[
settings.ollamaLocal.defaultModel,
'qwen3.5',
'glm-4.7-flash',
]
.map((item) => item.trim())
.where((item) => item.isNotEmpty)
.toSet()
.toList(growable: false);
}
List<String> mergeOptionsInternal(String current, List<String> defaults) {
return <String>[current.trim(), ...defaults]
.map((item) => item.trim())
.where((item) => item.isNotEmpty)
.toSet()
.toList(growable: false);
}
List<String> getArchitectModelOptionsInternal(
SettingsSnapshot settings,
MultiAgentConfig config,
) {
return mergeOptionsInternal(config.architect.model, <String>[
'kimi-k2.5:cloud',
'qwen3.5:cloud',
'glm-5:cloud',
...getLocalModelOptionsInternal(settings),
]);
}
List<String> getLeadModelOptionsInternal(
SettingsSnapshot settings,
MultiAgentConfig config,
) {
return mergeOptionsInternal(config.engineer.model, <String>[
'minimax-m2.7:cloud',
'qwen3.5:cloud',
'glm-5:cloud',
...getLocalModelOptionsInternal(settings),
]);
}
List<String> getWorkerModelOptionsInternal(
SettingsSnapshot settings,
MultiAgentConfig config,
) {
return mergeOptionsInternal(config.tester.model, <String>[
'glm-5:cloud',
'qwen3.5:cloud',
'glm-4.7-flash',
'qwen3.5',
...getLocalModelOptionsInternal(settings),
]);
}
List<Widget> buildExperimentalInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
UiFeatureAccess uiFeatures,
) {
final toggles = <Widget>[
if (uiFeatures.allowsExperimentalSetting(
UiFeatureKeys.settingsExperimentalCanvas,
))
SwitchRowInternal(
label: appText('Canvas 宿主', 'Canvas host'),
value: settings.experimentalCanvas,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(experimentalCanvas: value),
),
),
if (uiFeatures.allowsExperimentalSetting(
UiFeatureKeys.settingsExperimentalBridge,
))
SwitchRowInternal(
label: appText('桥接模式', 'Bridge mode'),
value: settings.experimentalBridge,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(experimentalBridge: value),
),
),
if (uiFeatures.allowsExperimentalSetting(
UiFeatureKeys.settingsExperimentalDebug,
))
SwitchRowInternal(
label: appText('调试运行时', 'Debug runtime'),
value: settings.experimentalDebug,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(experimentalDebug: value),
),
),
];
return [
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('实验特性', 'Experimental'),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
if (toggles.isEmpty)
Text(
appText(
'当前发布配置未开放额外实验开关。',
'This build does not expose additional experimental toggles.',
),
),
...toggles,
],
),
),
];
}
}