用途:Marionette 让 AI 智能体像真实用户一样驱动运行中的 Flutter 应用,用于验证功能、复现与确认缺陷、走查重构、无障碍检查、表单校验,以及为 PR 或工单生成截图/录屏证据。仅支持 debug 或 profile 构建,依赖 Dart VM Service,release 二进制中不存在,因此不可用于 release。
组成:应用侧包 marionette_flutter 为每个动作注册 VM service 扩展(ext.flutter.marionette.*);桥接端为 marionette_mcp(MCP 工具,如 connect、tap、get_interactive_elements)或 marionette_cli(shell 命令,如 marionette tap --key ...)。两者能力接近,仅传输方式不同;CLI 额外提供 record-video,MCP 无对应能力。工具列表中有 MCP 工具时优先使用,受限环境(企业策略、仅 shell 的智能体)回退到 CLI,并在会话开始时运行 marionette help-ai 按其参考确认语法。
准备应用:connect 或任何 CLI 命令仅在目标应用初始化了 MarionetteBinding 时可用。添加依赖:flutter pub add marionette_flutter。在 main() 中按 kDebugMode 分支调用 MarionetteBinding.ensureInitialized(),否则调用 WidgetsFlutterBinding.ensureInitialized()。改动 main() 后需热重启,热重载不会重新执行 main()。所有 Marionette 接线都应置于 kDebugMode 或 !kReleaseMode 之后,避免进入 release 构建。
自定义设计系统:零配置仅识别标准 Material 组件,且只从 Text、RichText、EditableText、TextField、TextFormField 提取文本。若应用封装了自己的按钮、输入框或文本,get_interactive_elements 结果会稀疏,tap(text: ...) 也找不到屏幕上明显存在的目标。可通过 MarionetteConfiguration 的 isInteractiveElement 与 extractText 识别自定义组件并提取可见文本,作为参数传给 MarionetteBinding.ensureInitialized(MarionetteConfiguration(...))。isInteractiveElement 与 extractText 接收 Element,可用 is 或模式匹配 element.widget,以覆盖泛型与子类。已弃用的 isInteractiveWidget 与 shouldStopTraversal 应迁移到 isInteractiveElement / shouldStopTraversalAtElement。自绘文本与徽标不会到达 Text 组件,可用 Semantics(label: ..., value: ...) 标注。不必为每个自定义组件都加条目,只在它是主要交互原语或明显缺失时添加。shouldStopTraversalAtElement 除非有实测成本,否则保持 null,且不要指向滚动容器。
日志:没有 LogCollector 时 get_logs 返回添加说明而非实际日志。使用 logging 包可 flutter pub add marionette_logging 并传 LoggingLogCollector();使用 logger 包可 flutter pub add marionette_logger 并传 LoggerLogCollector();其他情况用 marionette_flutter 自带的 PrintLogCollector,手动调用 addLog(message)。
截图:take_screenshots 默认将截图缩小到 2000×2000 物理像素以内,可通过 MarionetteConfiguration 的 maxScreenshotSize 覆盖,或设为 null 禁用缩放。
设备配置扫描:set_device_config 可在不修改真实 OS 设置的情况下扫描文字缩放、粗体、明暗外观,但需要树中存在组件,应用需通过 MarionetteDeviceConfig 选择加入,否则返回设置说明。
单绑定规则:Flutter 每进程只允许一个 WidgetsBinding。flutter test 在 kDebugMode 下运行 main() 会与 AutomatedTestWidgetsFlutterBinding 冲突,可用 Platform.environment.containsKey('FLUTTER_TEST') 防护或为测试使用单独入口。Sentry 是已知的静默冲突方:SentryFlutter.init() 会在你的代码前调用 WidgetsFlutterBinding.ensureInitialized() 并吞掉绑定错误,导致应用卡在启动屏。修复方式是初始化顺序:先调用 MarionetteBinding.ensureInitialized(),再调用 SentryFlutter.init()。
版本对齐:MCP 的 connect 会检查 marionette_mcp 与应用 marionette_flutter 版本一致,不一致时给出明确错误;CLI 不做此检查。对齐方式:应用内 flutter pub add marionette_flutter,桥接端 dart pub global activate marionette_mcp/marionette_cli,或作为开发依赖 dart pub add dev:marionette_mcp/dev:marionette_cli。
连接:每个动作都需要 VM service WebSocket URI(ws://127.0.0.1:PORT/TOKEN=/ws),可从 flutter run 控制台或 DevTools 链接获取,先传给 connect(MCP)或 --uri / 已注册的 -i <name>(CLI)。存在多个运行实例时不要猜测,根据对话推断或询问。CLI 一次性会话优先 --uri,连续多条命令可用 marionette register <name> <uri> 加 -i <name>。MCP 会话结束调用 disconnect。connect 可传 session_title(约 60 字符内,英文),若应用启用 enableSessionReports: true 会打开新的会话目录记录步骤日志与截图;一次运行对应一个会话目录,不可恢复,中断即废弃,不要复用标题。
连接后可用的动作:检查类 get_interactive_elements、take_screenshots、get_logs;手势类 tap、secondary_tap、double_tap、long_press、swipe、pinch_zoom、scroll_to、press_back_button,匹配优先级为 key › identifier › text › type › coordinates,secondary_tap 仅桌面端;文本输入 enter_text、press_key,enter_text 直接覆盖字段值,press_key 发送真实按键事件但仅桌面/Web 可原地编辑;设备配置 set_device_config。
不适用场景:CI 或需要可重复的回归套件(手势是尽力模拟而非确定性插桩,应改用 Patrol);release 构建;性能测量(应使用 Flutter DevTools)。