news 2026/9/21 20:40:49

GSY GitHub App 冒烟操作手册:基于 mcp_dart 与 VM Service 的 Flutter 运行时验证实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GSY GitHub App 冒烟操作手册:基于 mcp_dart 与 VM Service 的 Flutter 运行时验证实践

GSY GitHub App 冒烟操作手册:基于 mcp_dart 与 VM Service 的 Flutter 运行时验证实践

【免费下载链接】gsy_github_app_flutterFlutter 超完整的开源项目,功能丰富,适合学习和日常使用。GSYGithubApp 系列的优势:我们目前已经拥有 Flutter、Weex、ReactNative、Kotlin View、Kotlin Jetpack Compose ,Compose MultiPlatform,Harmony ArkUI 七个版本,功能齐全,项目框架内技术涉及面广,完成度高,持续维护,配套文章,适合全面学习,对比参考。项目地址: https://gitcode.com/gh_mirrors/gs/gsy_github_app_flutter

本文是 gsy_github_app_flutter 仓库中tool/ai/smoke/冒烟操作手册的完整讲解,核心回答一个问题:在 Flutter 项目中,如何用可复核、可自动化的方式验证"UI 渲染 / 文案 / 事件行"级别的改动。你会读到这套冒烟体系的全部前置条件、装机命令、通用步骤模板、两种vm_service evaluate操控姿势的边界,以及仓库在 lib/app.dart 中为冒烟专门预留的顶层入口与防御性保底机制,读完即可照抄跑通 PR timeline、首页动态、仓库 Discussions 三个真实场景。

冒烟验证在 GSY 协作流程中的定位

在进入操作细节之前,需要先理解这套手册为什么存在。仓库根目录 AGENTS.md 的"运行时冒烟验证(强制)"章节给出了一条硬性规则:

"能编译过 + 装机不崩"不算测试通过。任何涉及运行时行为(UI 渲染、事件解析、状态流转、网络分支、多语言文案)的改动,在宣告完成前,author 必须在真机或模拟器上跑通对应改动路径,并把真实证据(截图 / 文案 dump / 错误日志)以文件形式产出并写清路径,禁止只凭"app 启动了、日志没红"就报完成。

AGENTS.md 随后把最低证据要求按改动类型分级:

改动类型最低证据要求
纯模型 / 纯工具函数flutter analyze+ 单测(若 test 目录已存在);无需截图
UI 渲染 / 文案 / 事件行至少 1 张真机截图 +mcp_dartwidget_inspector get_widget_tree命中目标 widget 或textPreview+get_runtime_errors无异常
关键路径(登录 / 网络栈 / 根装配 / 状态边界)主路径截图 + widget tree 命中 +get_runtime_errors无异常 + 至少 1 个失败/边界分支的证据

tool/ai/smoke/README.md正是为满足中间这一档("UI 渲染 / 文案 / 事件行")的最低证据要求而编写:它是一份冒烟操作手册,说明用mcp_dart该走哪条路径、该 grep widget tree 的哪几个命中项、该抓哪几张截图。

工具选型变迁:2026-09-02 全面转向 mcp_dart

手册开篇记录了一次重要的工具决策。历史上tool/ai/smoke/目录堆了一堆.sh/.ps1坐标脚本(基于adb shell input tap/swipe),2026-09-02 作者拍板全部删除,回归mcp_dart,理由有三:

  1. adb 只是 Android 平台工具,天然把 iOS 排除在外——GSY 用 iOS Simulator 冒烟时它一点忙都帮不上;
  2. 坐标硬编码脆弱:分辨率一变就全坏;系统条高度、键盘弹起、tab 数量变化都会导致 tap 落错(旋转 override / 状态栏拦截 / 讨论 tab 是否可见 / IDE 缩略图坐标 vs 物理坐标,都反复吃过亏);
  3. mcp_dart是随 Flutter 演进的一等公民:直接连 DTD/VM Service,能拉真实的widget_inspector get_widget_tree(含textPreview文案)、拉get_runtime_errors,跨平台、随版本演进、天然消除坐标依赖。

因此该目录不再放执行脚本,每个冒烟场景改为一份路径描述 md。这条决策与 AGENTS.md 的"工具选型"章节完全一致:mcp_dart是唯一主路径,adb/xcrun simctl只降级为"只截图"的工具,不再承担业务操作职责。

前置条件

开始任何冒烟场景前,必须满足四条前提:

  1. 设备(iOS Simulator 或 Android 真机 / 模拟器)已启动、flutter能识别到:
    • iOS:xcrun simctl list devices booted
    • Android:adb devices
  2. GSY app 已在设备上运行(debug 首选,release 也可以)。
  3. flutter run的 stdout 里能看到Dart VM Service on ... is available at: <uri>(debug 才有)。
  4. 已登录任意 fixture 账号(推荐CarSmallGuogho_token 只读)。

注意:mcp_dart依赖 DTD/VM Service URI,这条 URI 只在 debug 构建的flutter runstdout 里出现,所以冒烟自动化路径默认以 debug 构建为前提。

装机命令:为什么禁止 flutter install

手册特别强调一个装机反模式禁止使用flutter install。该命令内部走adb uninstall <pkg>+adb install,会顺手把/data/data/com.shuyu.gsygithub.gsygithubapp_flutter/下的全部 SharedPreferences 抹掉,TOKEN_KEY一并丢失——设备上等同强制登出,reviewer 无法直接复核 fixture。这条教训在 AGENTS.md 的"禁止行为"章节被固化为打回红线:2026 年装 discussions 冒烟版本时曾因flutter install清空了 CarSmallGuo 的 gho_ token,属于 author 责任事故。

正确的装机姿势:

iOS

flutter build ios --release # 装机走 Xcode 或 xcrun simctl install <UDID> build/ios/iphonesimulator/Runner.app

Android

# 1. 构建 release APK(首选 arm64,跟 CarGuo 主设备一致) flutter build apk --release --target-platform=android-arm64 --no-shrink # 2. 用 adb install -r 覆盖安装,保留 app data adb install -r build/app/outputs/flutter-apk/app-release.apk # ^^ 关键:-r = reinstall,保留 /data/data/<pkg>/

如果必须重装(例如包名或签名变了),先手动导出 token:Android 走run-as+cat shared_prefs/FlutterSharedPreferences.xml,iOS 走 Xcode Container 拷贝Library/Preferences/*.plist。装完可以用 GSY 登录页的 "Token 登录" 入口(见 login_page.dart)把 token 粘回来。

场景清单

手册固化了一份场景清单,每个场景一份 md,内含"目标 / fixture / 步骤 / 完成汇报必填 / 反例":

场景 md覆盖对象
open_pr_timeline.mdPR timeline 事件行 /reviewed body 卡片
open_home_dynamic.md首页 Dynamic tab / 事件识别 / 下拉刷新 + 上拉分页
open_repo_discussions_tab.md仓库详情 → 讨论 tab / discussion 列表 / 详情页 Markdown

执行者按步骤走一遍,把证据(widget tree 命中项 + 截图绝对路径 +get_runtime_errors结果)写进 AGENTS.md 完成汇报三段式(看代码 / 看编译 / 看运行)的"看运行"段。三段任一缺失 = 任务未完成。

通用步骤模板:7 步完成一次冒烟

手册给出了一套所有场景通用的步骤模板。这是一个 Flutter 项目:点击 / 触发 / 验证一律走mcp_dart(VM Service 一等公民),不基于adb/ 坐标 / 屏幕像素;adb/xcrun simctl只承担"截图"这一件事。

  1. 起 appflutter run -d <deviceId>,等 stdout 打印 DTD/VM Service URI。
  2. 连 DTDmcp_dartdtd listDtdUrisdtd connect <uri>
  3. 基线mcp_dartget_runtime_errors(应为No runtime errors found.)。
  4. 触发路由 / 交互(一等公民 =mcp_dartvm_serviceevaluate,详见下一节。
  5. 拉 treemcp_dartwidget_inspector get_widget_tree summaryOnly=true,在返回 JSON 里 grep 该场景 md 指定的textPreview或 widget 类型。
  6. 截图(仅人眼补充证据,不承担业务验证职责):iOSxcrun simctl io <UDID> screenshot <path>,Androidadb exec-out screencap -p > <path>
  7. 收尾mcp_dartget_runtime_errors再拉一次,应仍空。

第 4 步展开:触发路由的三种姿势

GSY 已经在 app.dart 把GlobalKey<NavigatorState> navKey声明为顶层 final 变量,挂在MaterialApp(navigatorKey: navKey)上;同时 app.dart 提供了一批kDebugMode保护的顶层 smoke 入口。这套组合意味着:vm_service evaluate里一行就能跳到任何目标页。

主路径(首选):拉一次Isolatelibraries[]uri == "package:gsy_github_app_flutter/app.dart"那条拿id作为targetId,然后evaluate一行:

evaluate( targetId: <library id of package:gsy_github_app_flutter/app.dart>, expression: 'gsySmokeGoIssueDetail("CarGuo", "gsy_github_app_flutter", "938")' )

关键辨析:<library id>package:gsy_github_app_flutter/app.dart这个具体 libraryid,从Isolate.libraries[]里查,不是Isolate.rootLibrary字段——Dart 中 "root library" 术语专指 isolate 入口 library(本项目是main.dart),两个概念不同。app.dart 的注释对这一点有完整解释,open_pr_timeline.mdopen_repo_discussions_tab.md两份场景 md 也反复提醒。

现有顶层入口清单(都在 lib/app.dart 底部,源码可见):

入口函数签名跳转目标
gsySmokeGoIssueDetail(owner, repo, issueNumber)issue / PR 详情(GSY 中两者复用同一 detail page)
gsySmokeGoReposDetail(owner, repo)仓库详情
gsySmokeGoDiscussionDetail(owner, repo, number)Discussion 详情(GraphQL 通道)
gsySmokeGoSearch({Offset centerPosition = Offset.zero})搜索页(route-topology 后 shellDetail 语义)
gsySmokeGoPerson(userName)个人页

这些函数全部有kDebugMode早退门,release 构建下只debugPrint一条忽略日志并返回,不承担业务逻辑。需要新用例就照现有 pattern 加一个Future<Object?> gsySmokeGoXxx(...)即可,不改其它任何文件。

页面内交互(下拉刷新 / 上拉分页 / tab 切换):这类不是路由,smoke 顶层入口默认不覆盖。仍走"抓对应StateobjectId、eval_pullLoadWidgetControl.onRefresh?.call()/_tabController.animateTo(3)"这种姿势。例如 open_home_dynamic.md 里,首页动态的下拉刷新走 gsy_pull_load_widget.dart 的GSYPullLoadWidgetControl,控制器实例实际挂在DynamicBloc上(见 dynamic_bloc.dart),evaluate 直接调用_pullLoadWidgetControl.onRefresh?.call()onLoadMore?.call()即可触发刷新和分页。如果反复冒烟同一场景,再考虑给 app.dart 加gsySmokeRefreshHome()之类顶层入口,让它内部走 eventBus 广播。

降级 A(旧姿势):如果 debug 构建因某种原因没有相应的gsySmokeGoXxx顶层入口,退回到"抓任意 ElementobjectId+_element!.buildContext+NavigatorUtils.goXxx"的老姿势:

evaluate( targetId: <element_object_id>, expression: ''' (() { final ctx = _element!.buildContext; return NavigatorUtils.goIssueDetail( ctx, "CarGuo", "gsy_github_app_flutter", "938", ); })() ''' )

如果 eval 的作用域拿不到NavigatorUtils(未 import),改用evaluateInFrame,并从Isolate.libraries[]里查uri == "package:gsy_github_app_flutter/common/utils/navigator_utils.dart"那条拿libraryId(同样不要用Isolate.rootLibrary)。

降级 B(最后的最后):人肉在 Simulator 上点。必须在完成汇报里说明"这一步为什么无法自动化"

为什么触发操作走 vm_service eval 而不是 adb shell input tap

手册专门用一节解释这条原则,核心逻辑:

  • GSY 是Flutter 项目,widget 是 Dart 世界里的对象。adb shell input tap X Y只是在系统层伪造触摸事件,命中的是"屏幕像素点",跟 Flutter 的 widget hit test 没有直接映射:分辨率变一变、系统条高度变一变、键盘弹起 / tab 数量变一变,全炸;
  • vm_serviceevaluate直接在 Dart 层执行表达式,等同于让 Dart 自己调用NavigatorUtils.goXxx/TabController.animateTo/ 任意 controller 方法,随 Flutter 版本演进、跨 iOS/Android、随 UI 微调不变,天生就是 Flutter 项目该有的操控姿势;
  • adb/xcrun simctl因此在本仓库里降级为只截图的工具,不再承担业务操作职责。

两种 eval 姿势的适用边界

主路径(顶层gsySmokeGoXxx()降级 A(Element + buildContext)
targetIdpackage:gsy_github_app_flutter/app.dart的 libraryid任意在线ElementobjectId
expression一行:gsySmokeGoIssueDetail("...", "...", "938")多行:(() { final ctx = _element!.buildContext; return NavigatorUtils.goIssueDetail(ctx, ...); })()
依赖debug 构建,app.dart里现有的gsySmokeGoXxx()顶层函数任意已挂载 widget 的 Element 存在 +NavigatorUtils在 eval 作用域可解析
覆盖场景route 类跳转(issue / repo / discussion / person)任意 State 内部字段 / controller / 私有方法调用
推荐度首选(一行、可读、reviewer 直接看得懂)只在 debug 构建没有对应gsySmokeGoXxx顶层入口时用
release 副作用无(kDebugMode早退 +debugPrint无(release 版跑 eval 本来就不成立)

结论:能加顶层入口就加顶层入口。目前 4 个 route 入口够 PR / 仓库 / discussion / 用户页 4 大场景;之后要覆盖新 route 时照现有 pattern 追加一个Future<Object?> gsySmokeGoXxx(...)就行——不改其它任何文件,reviewer 也一眼看得懂"这就是一个 debug-only 顶层函数"。

smokePostFrame:evaluate 时机上的防御性保底

值得深入的是 app.dart 中的smokePostFrame<T>函数——它是所有gsySmokeGoXxx共用的 push 时机保底。其设计基于两条官方语义硬事实:

  1. 只要当前schedulerPhase == idle,直接同步Navigator.push完全安全,push 触发的setState会正常scheduleFrame
  2. 只要当前schedulerPhase != idle,说明当前一定有一帧在跑,Flutter 保证帧末尾 flush post-frame callbacks,所以在addPostFrameCallback里执行动作一般不会挂死。

同时它做了异常透传处理:用Completer.completeError让异常沿Future冒到 evaluate 侧(VM Service 那头看到ErrorRef而非"正常完成的 Future",避免"evaluate 无异常但页面没跳"的假阳性),并用FlutterError.reportError把异常汇报给全局错误通道,mcp_dart get_runtime_errors能直接捞到。

这个函数的四条分支(navKey null 早退 / idle sync throw / idle async reject / post-frame path)都有单测守约,见 test/app/smoke_post_frame_test.dart:它通过可控 scheduler 的testWidgets覆盖了真机上很难自然触发的 post-frame 分支,这正是 AGENTS.md "稀有分支覆盖率无法靠真机保证时,优先加模型层单测"条款的直接应用。

evidence/ 目录约定:证据如何留档与汇报

  • 默认 evidence 落到tool/ai/smoke/evidence/<yyyymmdd_hhmm>/,已通过根 .gitignore 里的tool/ai/smoke/evidence/忽略,不入 git;
  • 建议按任务号建子目录(例:evidence/c1/evidence/d1_selftest/),在完成汇报里把子目录绝对路径贴出来,reviewer 就能定位到当次证据;
  • PR / 完成汇报必须内联贴摘要(不能只写"证据在 evidence/xxx/"就交差)。因为 evidence 目录不入 git,reviewer 拉取 PR 时看不到里面的东西,所以汇报正文必须手动摘录足够的关键片段,reviewer 才能不 checkout 就完成 review。摘要至少覆盖:
    • get_runtime_errors前后对比:改动前基线 + 关键路径跑完之后各拉一次,贴errorsSinceLastRequest.length和相关 error 的renderedErrorText首 3 行(如无新增就写errorsSinceLastRequest=[]);
    • widget_inspector 命中项:命中的 widget 类型 / 关键textPreview字符串(例Copilot 提交了评审意见),至少给出行内引号完整包住的一行;
    • 截图必须以 PR/issue 附件或 Markdown 内联图片形式贴出来——只写本地绝对路径 reviewer 打不开(evidence 已 gitignore)。绝对路径只作为作者自留档索引;如果用 iOS / Android 平台的分享上传(Slack / 飞书 / 邮件附件)也可以,只要 reviewer 不 checkout 就能拿到图;
    • 无法覆盖的分支列表:显式列成 bullet,不要糊成"通过"。

反例:这些做法会被 reviewer 直接打回

手册列出一份明确的禁止清单,与 AGENTS.md 的禁止行为章节互为呼应:

  • 新增adb shell input tap/swipe坐标脚本:本次全面清理的历史包袱,reviewer 见到直接打回。Flutter 项目触发操作走vm_service eval,不基于像素点
  • 把"人肉在 Simulator 上点"当默认路径:默认路径永远是mcp_dartvm_service eval。只有 eval 走不通 + 降级 A(debug-only 顶层函数)也走不通,才允许降级 B(人肉点),且必须在完成汇报里说明"这一步为什么无法自动化";
  • 只截图不连 DTD:截图只是"人眼层面补充",不是业务证据;必须配widget_inspector命中和get_runtime_errors结果;
  • flutter install装机:见"装机命令"章节;
  • 让用户手动操作 UI 代替自己自测:author 必须自己走完路径;
  • 拿"日志里没 Exception"当行为正确的证据:必须命中widget_inspector

本目录不做什么

三个边界约束,保证这套冒烟手册职责单一、不与其它测试手段重叠:

  • 本目录 md不做断言(要不要过看的是widget_inspector命中 + 截图 +get_runtime_errors);
  • 本目录 md不 mock 数据(要覆盖稀有事件分支请写单测 + JSON fixture,例如 test/model 目录下的模型层单测);
  • 本目录 md不依赖flutter_driver(本仓库未引入相关依赖,因此 AGENTS.md 中flutter_driver_command子工具默认不可用)。

历史勘误:commit 224a0d8 的编造因果挂账

手册末尾挂了一笔重要的历史勘误(errata),用于纠正对vm_service evaluate时序语义的错误认知——这条订正已经同步写入 AGENTS.md 的禁止行为章节("在文档 / commit message / code comment 里编造 VM Service / mcp_dart 时序细节")。

  • 编造内容:commit224a0d8body 的「看代码」段落里写了mcp_dart vm_service evaluate是同步塞进 isolate 当前任意回调栈里跑的,会撞进 build/layout/paint/semantics 遍历,直接Navigator.push就抛 "Build scheduled during frame",并把_smokePostFrame定性为"从根上修 evaluate 时机,不是补丁";
  • 实际语义(对照 Dart VM Service Protocolservice.mdevaluate章节与官方文档):evaluateRPC 只承诺在目标 isolate 的事件循环里排队执行表达式,从未承诺"同步塞进任意回调栈中间";Dart isolate 是单线程消息循环,跨进程注入的表达式必须等当前 message 处理完才轮到,不可能同步打断 build/paint 半程。若要真正做到"在栈中间求值",走的是另一个 RPCevaluateInFrame,且需 isolate 处于 paused 状态;
  • 实际因果_smokePostFrame为何保留):Navigator.push触发的setState仍有可能在 evaluate 排到的那一轮 message 结束、下一帧 layout/paint 开始时才被WidgetsBinding._handleBuildScheduled感知,存在边缘触发 "Build scheduled during frame" 的可能,addPostFrameCallback只是防御性保底(最多多等 ~16ms)。语义上是"防边缘炸",不是"必须的根因修复";
  • 订正 commited7077e已把 lib/app.dart 里对应的 doc comment 重写,把「必要修复」降级为「防御性保底 + 官方语义引用」,代码行为无改动。224a0d8的 commit message body 由于 git 历史不可变,保留原文但以本条 errata 显式挂账——凡看git log 224a0d8的人务必对照本节修正对因果的理解。

小结:一份可直接落地的 Flutter 冒烟方法论

tool/ai/smoke/README.md出发,可以看到 GSY 仓库把"运行时冒烟验证"从一句口号落地成了一整套可执行的工程规范:以mcp_dart(DTD/VM Service)为一等公民,用widget_inspector的真实 widget tree(含textPreview)作为业务证据,用get_runtime_errors作为运行时健康基线,用adb/xcrun simctl只做截图补充,配合 lib/app.dart 中kDebugMode保护的顶层 smoke 入口实现"一次 evaluate、一行跳转"的自动化路径,最后以 evidence 目录 + 完成汇报三段式保证 reviewer 不看设备也能复核。这套方法论不仅适用于本仓库,对任何"必须验证 UI 渲染与文案而不仅仅是编译通过"的 Flutter 项目,都具备直接借鉴价值——关键动作就是三条:装别用flutter install、触发走vm_service eval、证据必须命中widget_inspector

【免费下载链接】gsy_github_app_flutterFlutter 超完整的开源项目,功能丰富,适合学习和日常使用。GSYGithubApp 系列的优势:我们目前已经拥有 Flutter、Weex、ReactNative、Kotlin View、Kotlin Jetpack Compose ,Compose MultiPlatform,Harmony ArkUI 七个版本,功能齐全,项目框架内技术涉及面广,完成度高,持续维护,配套文章,适合全面学习,对比参考。项目地址: https://gitcode.com/gh_mirrors/gs/gsy_github_app_flutter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 20:38:37

Java与PHP核心技术对比与选型指南

1. 语言背景与定位差异Java和PHP作为两种截然不同的编程语言&#xff0c;各自在技术生态中占据着独特位置。Java诞生于1995年&#xff0c;最初被设计为一种"编写一次&#xff0c;到处运行"的通用编程语言&#xff0c;其强类型、面向对象的特性使其在企业级应用开发中…

作者头像 李华
网站建设 2026/9/21 20:35:15

解决Lombok @Getter注解失效的排查指南

1. 问题现象与背景分析最近在Java项目中使用Lombok的Getter注解时遇到了一个奇怪的问题&#xff1a;明明在类上添加了Getter注解&#xff0c;但在调用getCode()方法时却报"找不到符号"的错误。这个问题看似简单&#xff0c;却困扰了我整整一个下午。经过排查发现&…

作者头像 李华
网站建设 2026/9/21 20:34:40

SpringBoot+Vue构建流浪动物救助平台实战

1. 项目概述与背景流浪动物救助平台是一个典型的Java Web全栈项目&#xff0c;采用SpringBootVue技术栈实现。我在实际开发过程中发现&#xff0c;这类系统最核心的价值在于解决了传统救助方式中的三个痛点&#xff1a;信息孤岛、流程混乱和资源浪费。平台前端使用Vue 2.x Ele…

作者头像 李华
网站建设 2026/9/21 20:33:58

GitHub Trending爬虫开发:自动化追踪热门开源项目

1. 项目背景与核心价值GitHub Trending作为全球开发者关注的开源风向标&#xff0c;每天都会根据star增长数、fork数等指标动态更新热门项目榜单。对于开发者而言&#xff0c;及时获取这些信息意味着&#xff1a;第一时间发现技术领域的新趋势&#xff08;比如突然爆火的AI工具…

作者头像 李华
网站建设 2026/9/21 20:31:24

Linux USB协议栈框架剖析:从枚举到驱动开发与调试

做Linux开发这些年&#xff0c;我接触过不少新人&#xff0c;几乎每个人第一次面对/sys/bus/usb/devices/下面那一长串以数字命名的目录时&#xff0c;都会陷入同一个困惑&#xff1a;内核到底是怎么把这棵树搭起来的&#xff1f;USB设备从插入到能被应用程序访问&#xff0c;中…

作者头像 李华