关于 fr-mvvm-contract
fr-mvvm-contract 是一项指导 Flutter 项目搭建、契约开发、UI 复用和设计还原检查的技能,面向遵循其项目结构与工作流的开发者。
主要功能
- 01搭建与适配 Flutter 项目
- 02解析契约任务说明
- 03发现并提取可复用 UI
- 04检查 Figma 还原度与版本
- 05管理导航壳层归属
适用场景
- 01搭建新的 Flutter 项目
- 02适配现有 Flutter 项目
- 03查找并提取共享 UI
- 04对照 Figma 审查页面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ hu-wentao/ flowr --skill fr-mvvm-contract安装后在 Claude Code 中输入/fr-mvvm-contract确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/acdd_scaffold/lib/app_router.dart.tmplmain.dart.tmplassets/acdd_scaffold/lib/core/app_env.dart.tmplapp_locale.dart.tmplapp_theme.dart.tmplinterceptors.dart.tmplproviders.dart.tmplassets/acdd_scaffold/test/application_test.dart.tmplreferences/acdd_scaffold.mdadapt_project.mdapi-contract-semantics.mdaudit_figma_fidelity.mdbff-dual-authority.mdcheck_app_info.mdcontract-first-refactor-plan.mdextract_shared_ui.mdfigma_fill_data.mdfigma_flutter_design_to_code.mdfigma_release_management.mdfigma-node-binding.mdfigma-screen-audit.mdfr-acdd-install.mdfr-acdd.mdfrontend-interactions.mdgen_component.mdgen_page.mdgenerate_openapi.mdnavigation-shells.mdoptional-build-packaging-optimizations.mdpackage_bff.md详细介绍
FR MVVM 合约
使用 uv run --script <path> 运行每个捆绑的 Python 入口点。使用 uv add --script <path> <dependency> 添加或更新其 PEP 723 依赖项;切勿使用 python、python3 或 uv run python 调用捆绑脚本。
模式选择
- 对于新项目或
acdd_scaffold,请阅读references/acdd_scaffold.md并使用scripts/acdd_scaffold.py。在目标项目 存在之前,不要运行项目配置解析器。 - 对于需要采用标准脚手架结构的现有 Flutter 项目,请运行:
uv run --script <skill-root>/scripts/resolve.py --task adapt_project
按照解析出的清单、映射、审批、迁移和验证工作流执行。将此技能的
assets/acdd_scaffold/ 模板和 references/acdd_scaffold.md 中的边界视为标准。切勿对现有项目运行
acdd_scaffold.py --apply。
- 若要检查现有 Flutter 项目与常见的完整 Android/iOS 应用相比,仍缺少哪些应用级配置,请运行:
uv run --script <skill-root>/scripts/resolve.py --task check_app_info
阅读解析出的 references/check_app_info.md 说明,并报告每个必需项的
证据。对于无法通过仓库文件证明的开发者账号和证书,应视为未经外部验证;检查期间不要修改标识符、签名、平台目标、图标或推送配置。
- 对于现有项目中的合约工作,请运行:
uv run --script <skill-root>/scripts/resolve.py --task <gen_page|gen_component|extract_shared_ui|validate|validate_routes|validate_navigation_shell|audit_figma_fidelity|audit_figma_data|refresh|package_bff|generate_openapi>
每个 instructions_id 只读取一次解析后的指令。
- 对于后端 OpenAPI 到 Retrofit 的生成,使用任务
generate_openapi运行解析器,读取references/generate_openapi.md,并使用scripts/openapi_to_retrofit.py。项目特定的泛型请求和响应包装器应放在.agents/skills-config/fr-mvvm-contract/config.yaml中; 不要在可复用技能中推断或硬编码其非泛型字段。 - 对于需要依据 Figma 审核或修复实现的屏幕,解析
audit_figma_fidelity,读取其解析后的项目配置,并在修改前后运行声明的审核命令。根据每个主.c.dart合约中的Figma Fidelity:判定,发现屏幕参与情况;不要维护第二份页面注册表。将视口、资源锁定路径和回归测试负责人保留在.c.dart中。仅在.agents/skills-config/fr-mvvm-contract/下的引用资源锁定文件中保留确切的导出标识、仓库路径和哈希;不要在此可复用技能中硬编码某个产品屏幕。实现或批准屏幕之前,先阅读references/figma_flutter_design_to_code.md,将强制要求的 Figma MCP 获取技能限定为证据收集,然后遵循references/audit_figma_fidelity.md,对导出的 SVG 资源运行其 SVG 扫描和安全规范化流程,并保持图标放置框与视觉字形尺寸及精确排版不变。SVG 流程绝不会自动修复几何形状,结构审核通过也不代表视觉审核已获批准。 - 对于从 Figma Frame 派生的 macOS 窗口,在修改
MainFlutterWindow.swift之前,先确定该 Frame 指定的是 Flutter 内容视口还是可见的原生窗口框架。使用setContentSize和contentMinSize/contentMaxSize保持内容视口。如果 Frame 明确指定的是可见的 macOS 窗口,则改用setFrame、minSize和maxSize;内容尺寸约束会叠加标题栏高度,导致外部窗口过大。除非设计明确要求无边框窗口,否则保留原生标题栏行为,然后使用fvm flutter build macos --debug验证。 - 编辑任何受 Figma 约束的 Page 或组件之前,先阅读
references/figma_release_management.md,并针对其.c.dart运行scripts/resolve_figma_release.py。当项目配置声明了发布版本时,将active_release视为明确依据;绝不要通过对名称排序来推断最新发布版本。若结果为stale,则需在实现前轻量检查活动文件的结构,并迁移受影响的合约。只有当路由标识、业务职责、导航上下文、主 Frame 和状态 Frames 均指向唯一明确的后继版本时,才可自动迁移。后继版本缺失、拆分、合并或不明确时,必须阻止操作。仅遵循有充分理由的Figma Release Override;绝不要自动创建该覆盖项来掩盖版本偏差。 - 当所属
.c.dart声明了Figma:时,将文案、格式化、运行时数据绑定和布局约束编辑视为受 Figma 约束的工作。选择运行时文本布局之前,先解析audit_figma_fidelity并使用references/figma_flutter_design_to_code.md。不要将某个 Figma 示例值的宽度变成通用的运行时约束。 - 对于两个或更多共用持久底部导航的目的地,解析
validate_navigation_shell,阅读references/navigation-shells.md,并在更改 Pages 或路由之前运行声明的验证器。先对共享外壳进行分类,再将每个目的地作为独立屏幕审核。 - 在常规项目创建、适配、验证或修复过程中,不要提出命令行打包或依赖下载优化。只有用户明确要求构建或打包优化时,才阅读
references/optional-build-packaging-optimizations.md,展示相关选项,并在应用这些优化前取得目标仓库所要求的授权。
源码优先布局
根据复用范围选择归属位置和目录:
- 仅使用功能目录进行分组。将每个 Page/Component 模块放入与其基本名称匹配的独立叶子目录;切勿将不同模块的框架或
*.c.dart契约放在同一目录中。 - 将路由专属组件放在
lib/app/<feature>/<component-name>/下,紧邻其可选的页面适配器。 - 将多个路由共用的组件放在
lib/components/<component-name>/下。不要在每个路由下重复放置。 - 将跨路由组件的所有实现产物放在其专属的
lib/components/<component-name>/目录中。不要在lib/core/中放置或保留模块专属文件。 - 仅在一个组件内部使用的 Widget,应在该组件的
.v.dart中设为私有。 - 在一个路由内复用的普通 Widget,应放在
lib/app/<route-segment>/widgets/下。 - 多个路由共用的普通 Widget,应放在
lib/widgets/下。 - 不要为普通展示类 Widget 提供契约、Provider、Event 或 ViewModel。当它封装了可复用的业务能力、应用/页面状态集成、独立状态、API、Event 或 ViewModel 职责时,将其提升为组件。
- 在已有明确等效根目录的项目中,除非显式的
adapt_project工作流批准迁移,否则应保留该根目录。保留根目录绝不意味着多个模块可以共用同一个叶子目录。
共享 UI 的发现与扩展
修改 Page 或路由专属组件之前,请按以下顺序查找可复用 UI:
- 在
lib/components/中搜索Capabilities:与所需能力匹配的组件。 - 在
lib/widgets/中搜索Capabilities:与所需能力匹配的共享 Widget 模块。 - 如果现有公开 View 或公开 Widget 满足需求,则复用它。
- 如果现有模块拥有该能力,但没有合适的公开入口,则在同一模块中添加语义明确的公开 View 或 Widget,并更新其公开列表。
- 仅当没有组件拥有所需的有状态能力或业务能力时,才创建新组件。仅当跨页面存在纯展示需求时,才创建 Widget 模块。
在作出此决定前,运行 scripts/discover_ui_reuse.py --project-root <project-root> --capability "<requested capability>"。将其输出视为目录,而非自动归属决策:创建新模块之前,应根据模块契约判断语义相近的匹配项。
提取共享 UI
当某个现有的、由路由拥有的 View 包含必须由另一条路由复用的 UI 条目时,请使用 extract_shared_ui。解决此任务时,请阅读
references/extract_shared_ui.md,并在更改 Dart 源文件前以 dry-run 模式运行
extract_shared_ui.py。该工具会生成一份可供审核的所有权和文件迁移清单;只有清单获批后,才允许使用 --apply。
根据所有权分类,而非外观是否相似:
- 仅包含输入和回调的展示条目,是
lib/widgets/下的共享 Widget;它没有 contract、Provider、Event 或 ViewModel。 - 具有独立状态、API、Event 或 ViewModel 的条目,是
lib/components/<name>/下的功能组件,并遵循完整的gen_componentcontract 工作流。不要使用 Widget 提取器机械地移动此类条目。 - 具有不同视觉或交互 contract 的相似 Widget 应保持独立,直到获批的设计决策定义出通用公共 API。
提取后,请在共享 Widget provider 中添加 Capabilities: 和 Public Widgets:,更新消费者的 Widget Tree: 条目,重新运行复用发现,并验证格式、测试和分析。
Capabilities: 和公共 API 列表由 provider 模块负责:
- 组件 contract(
lib/components/<name>/<name>.c.dart)声明Capabilities:和Public Views:。 lib/widgets/下共享 Widget 模块的公共条目声明Capabilities:和Public Widgets:。- 公共列表中的每一项都使用方括号引用,且必须能从该模块的公共条目访问。
消费者 Page contract 不声明 Components: 或 Shared Widgets:。
其 Widget Tree: 记录 Page 实际组合的关键公共 Views 和 Widgets。Page 负责布局与组合;provider 模块负责可复用 UI 及其交互。
- 将紧密相关的 Pages 归入同一功能目录下的跨页面模块,但每个 Page/Component 模块都必须位于单独的子叶目录中。功能模块中与目录同名的导出模块必须记录
Pages:和Page Data Flow:。创建或重构此边界前,请阅读references/validate_routes.md。
持久导航壳的所有权
在将多个 Figma Frame 或路由根视为完整页面之前,先确定它们是否属于同一个持久导航壳。只要两个或更多目的地共用一个底部栏,就应阅读 references/navigation-shells.md。
- 一个导航壳拥有外层 Scaffold、持久顶部区域宿主和一个底部导航实例。
- 分支页面适配器保留路由输入和页面级 Provider;分支视图仅包含内容。
- 底部导航 Widget 接收选中状态和回调。它绝不导入分支页面适配器、执行导航或负责受保护入口的预检。
- 无需预检的根操作可从导航壳视图回调中执行导航。需要权限、验证、API、业务策略或异步预检的根操作,则使用由导航壳拥有的组件 ViewModel 和监听器;分支、底部导航和目标页面的 ViewModel 均不负责其门控逻辑。
- 默认使用
StatefulShellRoute.indexedStack,以便切换分支时不播放过渡动画,同时保留各分支独立的状态。 - 保留分支状态并不能确保查询数据保持最新。当导航壳分支拥有查询 API 时,应检测从非活动状态到活动状态的实际转换,并向保留的页面 ViewModel 分发既有的加载/刷新 Event。不要重复触发初始 Startup Event、在普通重建时刷新或重建 Provider。确保并发刷新采用最新结果。
- 零时长页面过渡或
NoTransitionPage并不能修复持久导航壳。 - 根级全屏路由和覆盖层应通过根导航器/覆盖层覆盖导航壳;分支 ViewModel 不应隐藏或重建导航壳界面元素。
一个可复用的功能组件是一个 Dart 库:
order_content/
order_content.dart
order_content.c.dart
order_content.v.dart
order_content.vm.dart # only for page- or component-owned state
order_content.srv.dart # SDK adapter over lib/api/gen
order_content.bff.md # required in BFF-JSON mode
order_content.freezed.dart # generated by Freezed
order_content.g.dart # generated by json_serializable
order_content.dart 负责所有导入和 part 声明。其 .c.dart、.v.dart 以及可选的 .vm.dart 文件使用 part of 'order_content.dart';,且绝不声明导入。在 .c.dart 中,每个契约部分都使用连续的 /// 文档注释;不要使用 /* ... */ 契约代码块。
将 .c.dart 视为源契约,而非 View 实现文件。在 part of 之前放置契约注释,之后仅放置稳定的契约类型,例如 Model、Event、DTO 和业务枚举(如有)。所有公开和私有的 Widget/View 声明、构造函数、build、FrView 及视觉组合都放在 .v.dart 中。新草稿应遵循此布局;迁移时,阅读者仍可识别 .c.dart 中声明的一个旧版 View。
页面由该组件以及一个可选的独立类型化路由适配器组成:
order_content.page.dart
适配器导入 order_content.dart,继承 GoRouteData,拥有页面级 FrProvider,并在其下构建 OrderContentView。它不是 Widget,也不是组件的 part。删除适配器会移除页面拥有的生命周期;由应用拥有的无状态组件仍可由其他页面、底部抽屉、标签页或对话框独立使用。
命名与所有权
XxxPage位于xxx.page.dart中,通过生成的$XxxPagemixin 扩展GoRouteData,将路由输入作为构造函数字段持有,并将其传入页面 ViewModel 工厂和/或普通XxxView字段。它负责页面作用域的 Provider,且仅当契约声明Startup Event: [XxxStarted]时才分发启动 Event。它不是 Widget。- 公共
*View入口位于.v.dart中,并由.c.dart中的Public Views:权威列出。一个组件可以公开多个语义 View。每个 View 消费上游页面/应用状态或普通输入,默认不创建 Provider。 - 对每个新生成的、带有
@FrAcddPage的公共 Widget,阅读references/widget-preview.md,并在其公共构造函数上添加配置完整的 Flutter SDK@Preview(...)。根据Figma.Frame确定name,根据@FrAcddPage命名空间确定group,根据权威视口确定size;并要求使用提供预览运行时上下文的公共包装器。 - 仅当此模块拥有页面或组件作用域的状态时,才在
.vm.dart中定义XxxViewModel extends FrBlocViewModel<XxxEvent, XxxModel>;所有外部写入均使用add(event)。 - 组件字段、模型、DTO、Events、BFF/服务声明以及组件契约都属于组件库。
- 将组件状态命名为
XxxModel,将 BFF 请求和响应边界命名为XxxBffReq和XxxBffRsp,将仅供 BFF 使用的嵌套数据命名为XxxDto。项目的请求数据封装配置文件可以明确允许使用根级XxxRequestDto代替XxxBffReq。 - 组件 Service 从
lib/api/gen导入具体 SDK 并直接调用。当 ViewModel 构造 SDK 请求时,允许为 SDK 请求类型定义语义typedef。默认保留响应签名原有的生成式 SDK 形式。每个别名都必须保留精确的 SDK 类型、字段、泛型和序列化结构。 - 当某个前端
BFF-UI-API条目与后端拥有的业务 API 具有相同 HTTP 方法和路径时,它属于同一个直接后端边界,而不是第二个 UI API。其XxxBffReq引用必须是生成式 SDK 请求负载的精确typedef;绝不能声明替代用的 Freezed DTO,也不能复制、添加、删除或重命名请求字段。多调用 UI 聚合必须使用单独获批的 UI 边界;如果它只是本地编排且没有独立的 UI HTTP 端点,则使用BFF-UI-API: -。 - 声明 Events 前,先确定交互的归属:
- 当操作不会改变持久化/业务/API 状态时,将 BuildContext、无条件且已知类型的 Page 导航、覆盖层、控制器和其他仅用于展示的操作保留在 View 回调中。这类操作通常不需要 Event,也不需要
Interactions:Flow。常见示例并非详尽列表;不要添加穷尽式操作语法验证。 - 仅当 ViewModel 负责模型/业务变更、API 或服务工作、验证、守卫/并发策略、重试/恢复或可观察的状态结果时,才使用 Bloc Event 并记录
Interactions:Flow。 - 将选择器、URL、分享和剪贴板操作视为边界情况。仅用于展示时,将平台调用保留在 View 中;涉及权限、验证、持久化、API 工作、业务策略或模型变更时,则通过 ViewModel 处理决策和结果。
- 如果进入 Page 前需要权限、验证、API、业务策略或异步/并发预检,点击操作只表达意图。使用
Trigger -> Event -> ViewModel preflight -> observable approved/blocked outcome + nullable semantic navigation signal -> View FrListener/FrConsumer -> typed Page navigation。将网关注入 ViewModel;绝不能在 StatefulWidget 中等待门控结果后直接导航。默认使用ignore-while-active。通过一次直接的emit(state.copyWith(...))原子地发出每个声明的阶段:Pending 包含信号重置;approved Success 同时包含实际获批结果和精确信号;blocked/exception 结果绝不设置该信号。 - 不要生成 Intent 或回调输出协议作为第二条状态通道。普通可复用 Widget 可以接收输入回调。
- 当操作不会改变持久化/业务/API 状态时,将 BuildContext、无条件且已知类型的 Page 导航、覆盖层、控制器和其他仅用于展示的操作保留在 View 回调中。这类操作通常不需要 Event,也不需要
- 对于业务/API 结果导航,或 ViewModel 负责实际状态/业务决策的
Uses: localFlow,使用Navigation: view-listener-on-success [Model].field = Enum.member。每个可空语义枚举信号只能由一个 Flow 持有。将其重置为null,作为 Pending State 的一部分;在 Success State 中仅设置精确的成员;绝不能在 Failure、其他 Flow/处理程序或未声明的 ViewModel 代码中赋值其field:命名参数。Local Flow 的独立 Success 结果不得是明显的field = state.field空操作。View 的FrListener/FrConsumer必须绑定精确的 ViewModel 和 Model 泛型类型,并且在精确的枚举成员花括号分支中导航前,只能使用以下之一:精确的 transition parent、精确的相等性提前返回守卫,或一个精确的 transition-and-member&&条件。ViewModel 不得持有 BuildContext、路由器类型、Page 对象或路由调用。没有后续导航时,保留Navigation: none。仅当页面入口无条件时,直接展示路由才可保留为不带 Event 或 Flow 的 View 回调。 - 将 Provider 放在状态所有者的生命周期内,并置于所有消费者的最低公共祖先处:
app-owned [AppViewModel]:根AppProviders持有它;组件直接消费该状态,不声明本地 VM、Event、Model 或 Provider。page-owned [XxxViewModel]:由xxx.page.dart创建,并分发可选的已声明 Startup Event;该页面子树中的每个 Widget 都可以消费它。none:组件使用输入/回调或 Widget 本地的临时状态,不声明 VM、Event、Model 或 Provider。component-owned [XxxViewModel]:仅在明确选择加入时使用;适用于具有独立生命周期且多个后代共享其状态的独立可嵌入复合组件。
- 跨路由共享组件不得绑定到特定路由的 Page ViewModel。状态确实为全局时,消费 app-owned 状态;否则接收普通输入/回调。
页面路由输入与组件输入
- 不要声明
XxxPageArgs。XxxPage是唯一的类型化路由输入模型;将路径、查询和$extra输入声明为其构造函数字段。 - 组件库(
xxx.dart及其部件)绝不能引用自己的同级XxxPage、生成的路由 mixin、GoRouterState,也不能导入自己的xxx.page.dart。它可以导入另一个目标路由的.page.dart,以使用其生成的 Page helper,或使用目标路由拥有的 PageExtra 进行类型化导航。 XxxPage.build会在其 ViewModel 工厂和/或XxxView上的普通命名字段中使用每个路由字段。- 不要声明名为
XxxArgs或XxxConfig的组件输入包装器。 - 仅通过 Page 的 Provider 工厂传入
XxxViewModel所需的路由字段。不要将 Page 路由对象传入组件库。 - 仅当多个非 URL 值必须通过
$extra一起传递时,才使用由路由拥有的XxxPageExtra。直接在目标xxx.page.dart中声明它,绝不要放在独立的模型文件中。它只能作为路由传输模型使用,不能作为领域数据或 ViewModel 状态。目标 Page 必须将其展开为普通 View 字段。使用@FrAcddFreezedJSON、重定向的const factory和fromJson声明它;添加 page.freezed.dart部件,并保留共享的生成文件.page.g.dart部件。配置一个由应用拥有的GoRouter.extraCodec,为每个 PageExtra 设置带标签的编码/解码分支。迁移期间保留已批准的 PageExtra 字段约定;序列化要求不代表可以更改字段名称、值、所有权或生命周期。所需结构请参阅references/typed-routing.md。
| 类型 | 文件 | 使用方 |
|---|---|---|
OrderContentPage.orderId | order_content.page.dart | 路径输入和路由系统 |
OrderContentPage.entryPoint | order_content.page.dart | 查询输入和路由系统 |
OrderContentView.orderId | order_content.v.dart | 仅供 View 在渲染需要时使用 |
OrderContentView.entryPoint | order_content.v.dart | 仅供 View 在渲染需要时使用 |
使用此标准转换结构:
@TypedGoRoute<OrderContentPage>(path: '/orders/:orderId')
class OrderContentPage extends GoRouteData with $OrderContentPage {
const OrderContentPage({required this.orderId, required this.entryPoint});
final String orderId;
final String entryPoint;
@override
Widget build(BuildContext context, GoRouterState state) => FrProvider(
(context) => OrderContentViewModel(
orderId: orderId,
entryPoint: entryPoint,
),
onCreated: (context, vm) => vm.add(const OrderContentStarted()),
child: const OrderContentView(),
);
}
Page 字段描述路由如何进入,并初始化由页面拥有的状态。普通 View 字段仅用于 View 必须在不读取该状态的情况下自行渲染的值。路由类型不得进入组件库。
Page 契约
xxx.page.dart 声明一个从路由到 Provider 再到 View 的适配器:
@TypedGoRoute<OrderContentPage>(path: '/orders/:orderId')
class OrderContentPage extends GoRouteData with $OrderContentPage {
/* route fields -> page ViewModel and/or View fields */
}
路由路径从 @TypedGoRoute 读取,主 View 则从 XxxPage.build 推断;不要在文档注释中重复这两项信息。主 View 可以组合任意数量的公共/共享组件和 Widget Tree: 中记录的 Widget;页面并不局限于一个组件。只有在每个变体都构建相同的主 View 时,页面文件才可以为不同 URL 声明额外的类型化 Page 变体;将与文件基名匹配的 XxxPage 保留为主入口。
类型化路由
- 新脚手架默认使用
go_router_builder。请将它和build_runner保留在dev_dependencies中;每个独立的xxx.page.dart都会生成自己的$appRoutes,根目录的app_router.dart会展开这些带前缀的列表。 - 添加或更改路由前,请阅读
references/typed-routing.md。 - 对于跨页面模块或 PageExtra 迁移,请解析
validate_routes,阅读references/validate_routes.md,并运行该模块的验证器。 - 对于持久的底部导航目的地,请解析
validate_navigation_shell;分支切换使用 shell 的goBranchAPI,不属于 Page 到 Page 的类型化导航。 - 将
XxxPage作为GoRouteData;不要另建XxxRoute或XxxPageArgs。它的build会创建页面作用域,并在其下构建主要的XxxView。 - 当目标页面在应用代码中已知时,使用生成的路由辅助方法进行导航。仅在明确的外部/动态 URI
边界保留原始 URI 导航。
validate_routes会拒绝 URI 与类型化 Page 匹配的固定context.go/push/replace调用和AppRoutes.xxx间接调用; 对于例外的兼容性边界,请使用references/validate_routes.md中要求的、说明理由的标记进行记录。
契约优先工作流
- 检查 Figma,运行共享 UI 调研,检查匹配的组件和
Widget 模块目录、附近用法以及 API 上下文。记录以下一项结果:
复用现有公开入口;因没有合适入口而扩展其所属模块;或因没有模块拥有该能力而创建模块。当
请求提供多个 Figma 节点或一个 Figma 容器节点时,先阅读
references/figma-screen-audit.md,并将每个提供的 URL 归类为主要 Frame、同一所有者的状态、视觉参考或明确排除项。 请求任何完整设计上下文前,先检查容器的结构。 先根据可见的页面标题文字查找请求的页面,再将每个匹配的文字节点解析到其所属的 Frame。仅将 Frame 名称视为 候选提示,因为设计师可能未更新名称,或名称与内容无关。 必要时根据层级、导航上下文、独特内容和渲染结果确认屏幕;不要将菜单项或嵌入式 标题误认为页面本身。将已确认的node-id、当前 Frame 名称和可见页面标题记录为一组关联信息。选择具体的 Frame, 不要把容器当作页面来读取。 起草前,先展示最终的逻辑页面/状态所有权映射。 当主要 Frame 共享底部导航时,还要展示它们的 shell 成员关系,并在选择 Page 或 Scaffold 所有权前,将 shell 根页面、分支子页面、根级全屏路由和覆盖层分类; 绝不要根据链接数量或视觉相似度推断路由或契约数量。 未提供具体 API 时,页面草稿默认采用BFF-JSON。 共享组件草稿默认采用local,且State Ownership: none。 阅读references/api-contract-semantics.md;定义 DTO 前,先绘制跨组件的数据流和业务流。 - 对于
gen_page,起草xxx.page.dart、xxx.dart、xxx.c.dart以及xxx.v.dart中可供审查的公开 View 存根:
uv run --script <skill-root>/scripts/draft_contract.py \
--name order_content --dir lib/app/order_content \
--figma-url <url> --figma-frame <exact-current-frame-name> \
--figma-page-title <exact-visible-page-title|none> \
--mode bff-json --route <route> \
--preview-width <width> --preview-height <height> \
--preview-wrapper <public-wrapper> \
--theme <none|material|app-shared|component>
选择视口和包装器前,请阅读 references/widget-preview.md。
如果公开包装器在生成的组件库之外声明,请传入 --preview-wrapper-import <dart-uri>。生成器会拒绝缺少 Preview 尺寸或包装器输入的情况,而不会自行臆造。
对于 app-shared 或 component,还需传入 --theme-type <ThemeType>。
如果 Page 需要多个 $extra 值,请重复使用 --extra-field name:DartType。
草稿会生成必需的
@FrAcddFreezedJSON PageExtra、生成的部件、类型化 $extra,以及 PageExtra 到 ViewModel 的展开。
最终验证前,请在应用的路由 extra codec 中注册每个生成的类型。
路由专属组件使用 lib/app/<route-segment>/,跨路由复用的组件使用
lib/components/<component-name>/ --component-only。--component-only 默认使用 --mode local --state-owner none,因此不会生成 VM、Model、Event、Provider、Freezed、JSON 或 BFF 资产。若组件使用现有的根级 Provider VM,则使用 --state-owner app --state-type <AppViewModel>。只有在明确证明组件具有独立生命周期后,才使用 --state-owner component;组件拥有的 API/BFF 模式必须设置此标志。若现有项目使用不同的根目录,除非经批准的适配会调整这些目录,否则应沿用项目既定的等效目录。
3. 在进行契约审查前,将主 Figma Frame 和每个声明的 Figma States Frame 绑定回生成的 Dart 文件。绝不绑定 Figma References 或 Figma Excluded。在 Figma: 下记录主 Frame 当前的准确名称、可见的页面标题和完整的节点专属 URL,明确标示 node-id ↔ Frame name ↔ Page Title。仅当页面或组件有意不设标题时,才将 Page Title 记为 none。每个 Figma States 目标只记录其 node-id,并以主设计文件为准解析。
切勿在 Figma States 中重复设计 URL。对于已批准的屏幕,严格按照以下固定格式添加且仅添加一个 Figma Fidelity: 部分:
/// Figma Fidelity:
/// - Viewport: 360 x 780
/// - Asset Lock: .agents/skills-config/fr-mvvm-contract/order-figma-assets.lock.json
/// - Regression Test: orderFigmaFidelity renders approved states
仅当权威 Frame 没有导出资源时,才使用 Asset Lock: none。不要将 Figma 文件/节点、视口、路由、文案、源声明或测试名称写入锁定信息。不要向 Figma 写入合约元数据、共享插件数据或可见卡片。
如果当前实现明确不在已批准的门槛范围内,请改用单行 Figma Fidelity: excluded | <reason> 格式。
一次只准备一个页面合约和目标 Frame;页面合约必须指向其确切的 Figma Frame,绝不能指向包含多个页面的 Section。
在第二次 use_figma 调用中执行生成的
verifyCode,并检查其截图。
如果主 URL 缺少 node-id、页面目标不是 Frame、任一表示缺失或过时、卡片不在其页面上方、路径未以可见形式渲染,或独立回读结果不一致,请勿继续。
4. 在编写源自 Figma 的 View 之前,请阅读 references/figma_fill_data.md。
对 .c.dart 中 Figma Data: 部分的每个非文案 Figma 填充值进行分类。
草稿可以保留 TODO(figma-data) 标记或 Binding: pending 声明;合约和最终验证会拒绝这两者。不要将 Figma 示例值用作生产默认值。使用 scripts/figma_fill_data.py 报告整个项目中待处理、无效和尚未审核的旧声明。
5. 在内部将每个面向 UI 的 BFF 端点分类为 query 或 command;不要要求用户选择类型,也不要编写 API 类型字段。阅读 references/frontend-interactions.md。使用其中的请求边界作为端点标识,在 Behaviors: 下恰好编写一条记录,并编写一条端点范围内的 Request Field Sources: 记录;随后仅为由 ViewModel 拥有的 Trigger、Bloc Event、guard、状态阶段、并发和导航信号协调定义结构化 Interactions: Flows。不要将 View 本地回调转换为 Events 或 Flows。同一组件可以拥有 query 和 command 端点。不要在 .c.dart 中编写 SDK Calls、SDK Call Flow、后端方法/路径注释或后端编排。后端开发人员负责这些信息,且只能编辑 xxx.bff.md 中受保护的后端部分。该技能可以定义和刷新前端 UI 数据 API、UI DTO、状态、端点行为、交互 Flows、结构和集成映射。必须逐字节保留完整的后端部分。将 BFF Service 设为 Dart SDK 适配器服务类,例如 [OrderContentService];每个 BFF-JSON 合约都要求运行时集成。当数据边界未知或尚未提供所需证据时,添加 Data Boundary:,并使用稳定标记 TODO(data-boundary): <capability> — <missing authority/evidence>。不要将这种不确定性转化为 BFF-UI-API: -、示例数据行为或臆造的方法/路径。仅当明确批准不使用 API、仅限本地的决策后,才允许使用 BFF-UI-API: -。审批前搜索 TODO(data-boundary);合约和最终验证会拒绝未解决的标记。绝不要臆造 /bootstrap、nextRoute、proof、result 或 error 占位符。
使用已解析的 Contract Description Language 编写描述性合约值。这适用于 Behavior 值、交互状态/反馈说明、Request Field Sources 用途说明和 Notes。稳定标签、Flow ID、代码标识符、类型、方法、路径、枚举字面量和权威源表达式保持不变。
保持其余审批契约精简:Figma、API/BFF、状态归属、
Widget Tree 中可复用的 UI、主题、Event 和 ViewModel
引用、模型以及简明备注。Page Support 仅包含路由和
主要 View 事实。
将 Widget Tree 定义为关键 Widget 的简明层级结构,让
审核者仅凭 .c.dart 就能理解 View。包含直接可交互、
承载主要信息、体现重要状态、决定功能结构,或属于开发者
必须识别的共享组件的 Widget。优先选取 4–8 个关键
Widget;即使是复杂 View,也最多保留 12 个;将更大的树
折叠为业务级区域,对重复项使用 × N,并简要标注条件状态。
当 Public Views: 列出多个条目时,按相同顺序为每个公共
View 编写一个 Widget Tree 条目。不要仅为让语义不同的
条目通过一个通用 View 来添加判别枚举。
仅保留必要的层级。省略公式化的 _XxxViewBody 节点、
FrProvider、FrConsumer、Builder、Padding、
SizedBox、Spacer、Align、Expanded、Flexible 和
SafeArea 等布局辅助组件,省略 Divider 和
DecoratedBox 等纯装饰组件,以及父组件已涵盖的组件内部
标签、图标和间距。除非 Row、Column、Stack 和
Container 对区分业务结构至关重要,否则一律省略。语义明确
的私有 Widget(例如 [_HomeHeader])可以保留。
不要用 confirmation form 之类的文字替代关键 Widget 引用。
不要绘制 UI 示意图,也不要复现完整的运行时 Widget 树。
provider 模块的公共列表是依赖项/复用清单;简明的
Widget Tree 则记录此处实际组合的条目。在契约审核和审批
前,用内容明确、简洁的树替换生成的 TODO。
对于每条 Behaviors: 记录,移除未使用的查询或命令字段;
补全每条 Interactions: Flow,解决所有 TODO(data-boundary)
和其他待处理标记,然后定义 DTO 字段,并将类型化的 XxxPage
路由字段与最终普通 XxxView 字段同步。草稿外壳有意列出
尚未生成的部分,因此此审核阶段不构成编译或分析器检查门槛。
5. 列出每个 UI API 方法/路径及 Req/Rsp/Error、其端点
Behavior、端点范围内的字段来源、每个前端交互 Flow,以及
SDK 适配器服务类。非 BFF 本地组件可以明确声明仅由
ViewModel 管理的本地 Flow 的结构化 Interactions:;每个此类
Flow 均使用 Uses: local,且不需要端点、BFF Service、SDK
或 bff.md。不要展示、提议或编辑后端 API 或 flow;后端
开发者会在 xxx.bff.md 中维护它们。
6. 在推导文件之前验证已审批的源契约。此阶段会拒绝语义/API
占位符、缺失或不完整的端点 Behaviors、无法追溯的请求字段、
缺失的交互覆盖、无效的 Event/Widget/Model/响应引用、
仅用于 UI 的命令响应、无效的类型化 Page 路由字段转换、
不完整的 Theme 声明以及缺失的直接依赖,但此时尚不要求
Freezed/JSON 输出:
uv run --script <skill-root>/scripts/validate_contract.py \
--page-file path/to/xxx.page.dart --phase contract
- 对于所有非契约工作,通过脚本读取契约,而不是从原始 Dart 手动推导决策:
uv run --script <skill-root>/scripts/read_contract.py \
--page-file path/to/xxx.page.dart
uv run --script <skill-root>/scripts/read_contract.py \
--component-file path/to/xxx.dart
- 仅根据已审批的读取器输出准备派生部分。生成器会在提交任何
文件之前,预检完整契约、Theme 目标、依赖项和 BFF 提取器。
它会先准备 Theme 更改和 BFF 工件,再准备
.vm.dart与.v.dart存根,并将它们作为一个受回滚保护的文件集处理。 如果提取器或 Theme 失败,所有先前文件都必须保持不变:
uv run --script <skill-root>/scripts/generate_from_contract.py \
--page-file path/to/xxx.page.dart --write-stubs
每个包含运行时后端调用的 BFF-JSON 契约都声明
BFF Service: [Type]。每个组件使用一个 Service。Service 从 lib/api/gen 导入
所需的具体 SDK 文件,在内部构造已配置的通用包装器,并向其 ViewModel 暴露语义化方法。不要根据 UI 数据 API
生成前端 Retrofit 客户端。
项目可能要求每个 XxxBffRsp 都表示完整的网关响应,例如
{state, code, message, data}。在这种情况下,原始业务响应是其
data 字段的值,而不是外层封装的替代品。
生成的 *.bff.md 文件以精简的 bff-md-meta/v9 YAML Front
Matter 开头,其中包含 schema、@FrAcddPage 命名空间/版本、权威 UI 设计来源、带命名空间的 mdq v2 API 记录契约,以及不可推导的契约文件路径。文件将内联 UI
DTO 与由 OpenAPI 管理的后端操作、后端调用流程、前端 UI 数据、集成映射,以及生成的 API Query Records 验证投影分开。UI Contract 按以下顺序包含 UI State、endpoint Behaviors、生成的
前端交互逻辑 和 UI Structure。UI State 只能呈现为一个 JSON5 代码块:每个字段都必须依次包含 Model、Dart 类型和
Authority: Frontend 注释。UI State 不得使用 Markdown 表格。更改产物结构、
所有权、生成、解析或验证之前,请先阅读
references/bff-dual-authority.md。
BFF 产物必须按以下固定顺序呈现:标题、后端业务流程与业务逻辑 API 和
前端 UI 数据接口。后端域只包含 OpenAPI 文档引用、由后端开发人员编写的业务 API 注解和业务流程。
每条业务 API 注解仅保留方法/路径、参数名称和类型,以及响应 DTO 类型。后端开发人员可以在此受保护的域内其他位置添加说明文字、JSON、DTO 字段示例、schema 片段或代码块;解析器会保留这些内容,并将其排除在机器 API 记录之外。整个后端域只能由后端开发人员创建和编辑。AI 只能根据已批准的 Figma/UI
需求创建面向 UI 的路径和 XxxBffReq/XxxBffRsp DTO。
不得将后端业务 API 的方法/路径视为新的面向 UI 的路径。
如果方法/路径完全匹配,请求边界仍由 OpenAPI 管理,前端契约只能为其生成的 SDK 负载类型指定完全匹配的 XxxBffReq typedef。
生成器绝不会创建或覆盖 xxx.srv.dart。根据由后端管理的 BFF 流程,使用具体的 lib/api/gen 客户端实现它,然后从组件外壳导入。ViewModel 构造的请求类型可以获得语义化
typedef;除非响应本身必须作为已声明类型存储或传递,否则响应签名仍使用原始生成的 SDK 类型。Service 使用应用提供的 Dio,不得添加拦截器,也不得管理 base URL。先实现 .vm.dart,再实现 .v.dart。
--replace-derived-stubs 只能刷新仍包含其生成存根标记的文件;已弃用的
--force 具有相同的受限行为。
- 格式化手写的 Dart,运行 build_runner(包括 typed routes),在 Service/ViewModel 集成后刷新 BFF 验证投影,然后要求通过最终验证 和仓库分析器:
fvm dart format path/to/component/files
fvm dart run build_runner build
uv run --script <skill-root>/scripts/generate_bff.py \
--component-file path/to/xxx.dart
uv run --script <skill-root>/scripts/validate_contract.py \
--page-file path/to/xxx.page.dart --phase final
fvm flutter analyze
不得创建或持久化 JSON 规格文件。组件通过最终验证和分析之前,不得注册路由。
BFF 交付包
生成并更新所有组件 BFF 工件后,解析
package_bff 并运行其 package 命令。通用命令会将项目中的所有
*.bff.md 收集到 build/bff-contracts.zip,并保留相对路径。OpenAPI 文档仍由各自独立维护,作为引用文件,不会包含在 BFF 包中。有关排除项和项目配置,请阅读 references/package_bff.md。
允许项目配置覆盖 package,或声明可选的 sync 命令。将 sync 视为单独的外部变更:展示其目标位置和副作用,并在复制、提交或推送到其他仓库前取得明确授权。明确要求同步、发布或更新已配置的共享仓库,即为授权,其中包括向已配置分支的必要推送;绝不要再次请求推送确认。获得明确授权后,即使所有本地 *.bff.md 都是最新的,且 Git 报告没有 BFF 变更,也始终要在验证和打包后运行已解析的 sync 命令。本地内容是最新的,并不能证明目标位置内容一致;应让已配置的 sync 命令比较目标位置,并决定是否需要提交。解析器的执行绝不会授权或运行已配置的命令。
当已配置的目标位置是共享权威仓库时,必须严格使用以下交付结果:
packaged:交付归档仅存在于源仓库中。published:已回读已配置的远程分支,其提交与sync生成或选定的目标检出提交完全一致。
只有 published 才能满足同步、发布、交付或更新共享权威仓库的请求。本地文件、本地检出或本地提交绝不能作为仓库已更新的证据。如果没有发布授权,应报告当前尚未发布的状态并请求授权;不得声称已完成。如果已有授权,不要在提交后停止:推送并验证确切的远程分支和提交。命令成功但没有相应远程证据,不算交付结果,因此应报告任务未完成。项目配置可以定义哪些用户措辞表示授予发布授权;遵循该定义,不要再次询问,但绝不要推断授权涵盖范围更广的变更。如果推送会包含无关提交,应停止操作并报告范围冲突,不得以 sync 授权为由发布这些提交。
主题契约
- 仅使用
Theme: none、Theme: material、Theme: app-shared [ThemeType]或Theme: component [ThemeType]。 - 不要单独声明
Theme Ownership章节。app-shared和component会直接根据Theme选择fr_mvvm_theme的所有权。 - 将任何其他 Theme 文本视为旧格式。读取器可以显示迁移警告,但在迁移该声明前,验证和派生生成都必须停止。
- 当项目直接依赖
fr_mvvm_theme且经批准的契约使用app-shared或component时,加载flowr-usage技能。如果缺少 package 或根扩展注入,请阅读references/fr-mvvm-theme-install.md;否则请阅读references/fr-mvvm-theme.md。 - 在
lib/core下生成一个 app-shared Theme 类型,将其注册为具名的AppThemeModel字段,并将该FrPageTheme对象保留为顶层toJson()值。每个命名该类型的契约都复用同一个类型。 - 将组件所有的 Theme 生成为
xxx.thm.dart,并将其添加到组件外壳中。 - 使用
context.ofThm<ThemeType>()读取自定义 Theme 值。绝不要用abstract final class XxxColors替换经批准的FrPageTheme。 - 对于
material,从Theme.of(context).colorScheme读取共享语义颜色;不要生成页面颜色表或FrPageTheme。
验证
- 只要 Page 被声明为持久化 Shell 目的地,就运行
validate_navigation_shell。有效结果要求有一个 Shell 所有者、被动式底部导航、仅包含内容的分支 View、有状态的索引栈路由,以及有针对性的运行时覆盖。拥有查询的分支还要求 API 调用次数覆盖,以证明只有一次初始加载,并且每次从非活跃状态重新激活时仅发起一次新请求。仅断言最终状态的pumpAndSettle()无法证明切换过程中没有转场。 - 在
generate_from_contract.py之前使用--phase contract。只有在完成.srv/.vm/.v实现和 build_runner 后,才使用--phase final。省略--phase会保留旧版源代码验证行为以保持兼容;它不是最终完成门槛。 - 每个 Page/Component 模块都必须独占其叶子目录。验证会拒绝基名不同的同级模块 Shell 或
*.c.dart契约。 - 每个面向 UI 的 BFF 端点都有一条作用域限定在请求边界的
Behaviors:记录,其字段可供解析器推断query或command;一个 Component 可以同时拥有这两种类型。契约不公开 API 类型字段。每个端点都有一条作用域限定的Request Field Sources:记录,并且每个请求字段都声明一个权威来源和 UI API 用途。每个端点都至少被一个结构化的Interactions:Flow 使用。固定语法见references/frontend-interactions.md。 后端开发者在受保护的 BFF Markdown 区段中,将每个业务 API 记录为- [id] METHOD /path | Parameters: name Type[, ...] | Response: Type,然后从### 业务流程引用每个 id。该技能会根据 OpenAPI 和lib/api/gen验证这些注释,但不会编辑它们。保留后端编写的 JSON/DTO/代码示例,绝不可将它们投射到机器 API 清单或前端 DTO 中。 语法见references/api-contract-semantics.md。 - command 响应必须包含由
Success引用的非 UI 结果。UI/导航字段不能是 command 响应的唯一内容,并且每种失败都必须映射到 App 的恢复/展示处理。 - 每个包含后端调用的 BFF-JSON 契约都声明
BFF Service: [Type],指向xxx.srv.dart中唯一的 Component SDK 适配器类。该 Service 必须导入至少一个来自lib/api/gen的具体 SDK 文件,且不得使用@RestApi。验证还要求注入 ViewModel,并为每个 Flow 提供一个完全匹配的处理器。最终验证会证明每个 Flow 的 Event 分发/注册、守卫和并发策略、匹配的请求及等待的响应、pending/success/failure 状态写入,以及任何已声明的可空语义导航信号和对应的、由 View 持有的精确FrListener/FrConsumer转场分支。它会扫描 ViewModel 中可执行的信号field:赋值,拒绝出现在所属处理器或允许的 Pending/Success 区域之外的情况;保留对已声明状态写入的直接emit(state.copyWith(...))证明;并拒绝 BuildContext、标准 Flutter 路由器类型、带类型的 Page 调用,以及常见且有明显特征的 navigator/router 调用。对所有明确声明结构化Interactions:的非 BFF 本地 Component,也应用这些交互契约和最终运行时检查;本地 Flow 只允许Uses: local,且不要求端点、BFF Service、SDK 或bff.md。省略Interactions:或声明Interactions: none的本地 Component 仍保持兼容。这些静态检查可证明直接阶段发射、基本词法顺序和导航信号所有权,但无法证明 Dart 控制流中完全互斥。必须针对以下情况编写有针对性的 ViewModel 测试:被阻止的路径在不发出获准信号的情况下返回、异常处理、获准路径,以及活跃时的重复点击。继续对 BFF ViewModel 应用路由边界,包括声明Interactions: none的无 API Component。State Ownership: none仍然要求没有 ViewModel。不支持仅契约式 BFF 交付。 - Component 不得导入或引用其同级的
.page.dart适配器或同级 PageExtra。源 Component 可以依赖另一个目标 Page 适配器进行带类型的导航。 - 对于新模块和迁移模块,
Public Views:是权威清单;其中列出的每个类都必须是公共类,且由 Component 库声明;每个公开的*View都必须列出。Page 适配器仍从该清单中选择一个主 View。 Widget Tree:必须存在,并按声明顺序为每个公共 View 包含一个根节点,且每个根节点后至少包含一个关键 Widget。不得包含 TODO、公式化的_XxxViewBody、状态/实现包装器或确定性的布局/装饰噪声,非根 Widget 引用不得超过 12 个。app-shared和component必须指定一个 Theme 类型。该类型必须继承FrPageTheme<ThemeType>;app-shared类型必须在AppThemeModel中注册、通过toJson()保留为对象,并通过根级ThemeData(extensions: theme.data.extensions)路径注入。.v.dart不得为app-shared或componentTheme 契约静态引用XxxColors。material契约必须使用Theme.of(context).colorScheme。.c.dart不得声明名称以PageArgs、Args或Config结尾的类型来包装 Component 输入。.c.dart契约区段必须在part of前使用连续的///文档注释;块注释契约无效。公共和私有 View/Widget 声明应放在.v.dart中。- 每个主
Figma:契约都必须恰好声明一个Figma Fidelity:处置项。汇总发现会拒绝缺失/无效的处置项、不安全或重复使用的资源锁定路径、重复的已审计 Figma 绑定、未使用或未渲染的锁定资源,以及缺失的已声明回归测试或视口。Figma Fidelity: excluded | <reason>是明确的未获批准状态,不能作为视觉完成的证明。 - 当项目配置声明
figma.releases时,汇总发现还会解析每个具体契约文件键。gradual强制模式会将未设例外的已归档绑定报告为过期迁移工作;strict模式会拒绝它们。未知版本和候选版本、格式错误的覆盖项以及混合文件声明一律会失败。固定到已归档版本的发布必须使用与之匹配的版本和非空原因声明Figma Release Override。 - Component 源代码不得引用自身的
XxxPage、GoRouterState、生成的同级路由混入或同级.page.dart;允许引用跨路由目标 Page/PageExtra。 - Component 导航到内部已知的带类型 Page 时,必须使用
XxxPage(...).go/push/replace(context)。固定原始 URI 调用和AppRoutes.xxx均为无效替代方式,除非它们位于validate_routes定义且有理由说明的兼容性边界内。 - 每个
XxxPageExtra都必须使用@FrAcddFreezedJSONFreezed 结构、生成的 JSON 钩子以及应用路由 extra 编解码器。验证要求为每个传输的 PageExtra 显式覆盖编码器和解码器;只有toJson()而没有带类型的fromJson还原无效。 .page.dart声明XxxPage extends GoRouteData with $XxxPage,不包含XxxPageArgs,并在 Page ViewModel 工厂和/或普通 View 字段中使用每个路由字段。State Ownership: page-owned [XxxViewModel]要求每个带类型的 Page 变体都创建FrProvider;声明Startup Event: [XxxStarted]时,每个变体都要在.page.dart中分发该事件。Component View 不得创建另一个 Provider。State Ownership: app-owned [AppViewModel]和none会拒绝本地 Provider、VM part、Events 和 Models。component-owned [XxxViewModel]要求由 View 持有 Provider,并属于明确的例外情况。- 移除
.page.dart后,read_contract.py --component-file必须仍可正常工作。 - Page 适配器必须导入其同级 Component 库。每个带类型的 Page 变体都必须声明字符串字面量
@TypedGoRoute路径,并在其 Page 作用域下构建同一个公共XxxView;Route 和 Component 文档标记属于冗余内容,不得生成。 xxx.bff.md和xxx.srv.dart是 Component 资源,不是 Page 资源。xxx.srv.dart是由 Component Shell 导入的独立 SDK 适配器库;它不是 Component 的 Dartpart。 BFF-JSON 模式下必须提供xxx.bff.md,仅在显式 API 模式下省略。该文件以精简的bff-md-meta/v9YAML Front Matter 开头,后接其 BFF 契约标题,然后分别呈现后端逻辑、UI API Contract、UI Contract(包括端点 Behaviors 和前端交互逻辑)、Integration Mapping,以及由 mdq 支持的 API Query Records。从@FrAcddPage(version: ...)读取namespace和contract_version;注解字段准确名称为version,绝不可使用contractVersion,且产物架构版本不是契约版本。 UI State 必须使用一个 JSON5 代码块,而不是 Markdown 表格;每个字段都要附带 Model、Dart 类型和前端权威来源注释。API Query Records是专门为集合查询保留的唯一平面 GFM 矩阵。它必须明确呈现后端、UI、仅运行时和无 API 处置项,以及契约和集成状态。仅运行时行会报告缺少后端权威来源,但不会修改后端所有的区段。Service 或 ViewModel 更改后,generate_bff.py --check会拒绝过期行。- BFF-JSON 契约导入
fr_acdd,恰好声明一个@FrAcddPage(mode: FrAcddMode.bff)、至少一个根@FrAcddDto,并为每个 BFF DTO 使用@FrAcddFreezedJSON和fromJson。每个被引用的XxxBffReq(或明确设定配置的XxxRequestDto)还必须显式声明Map<String, dynamic> toJson();,以便进行确定性的 UI DTO 序列化;确切的直接后端 SDK 类型别名除外,其生成目标负责序列化。BFF-UI-API:指定面向 UI 的 HTTP 方法、路径、请求 DTO 和XxxBffRsp。仅在该 UI API 边界内部使用的 DTO 使用XxxDto。 将BFF-API:视为仅用于迁移的旧版别名。读取时保持语义不变,绝不生成该别名,并让常规生成或刷新过程将源标签和派生输出改写为BFF-UI-API:。保持--check只读,并报告迁移命令,而不是编辑契约。 后端操作不得在此区段中添加 Dart DTO。后端方法/路径/类型注释和流程只属于后端所有的 BFF Markdown 区段。Service 直接使用所引用的具体 SDK API;不得添加聚合 SDK 客户端或合成的后端边界。 - 在起草、生成、刷新或最终验证 BFF-JSON 契约之前,运行
ensure_fr_acdd.py --project-root <owning-package>,确保fr_acdd >= 0.7.0。对于缺失或过旧的托管依赖,它会尝试运行pub add;对于 path/git 依赖,它会尝试运行pub upgrade fr_acdd。如果解析后的 path/git 源仍然过旧,则停止并报告必须更新其源代码修订版本;绝不可暗中将 path/git 源替换为托管依赖。只有在明确进行只读验证时才使用--check。 - 使用
generate_bff.py --component-file path/to/xxx.dart [--check]生成或检查 BFF 交付内容。常规生成会在提取器预检前执行同样的fr_acdd >= 0.7.0检查和自动升级尝试。--check不会修改依赖:若需要升级,它会失败并提示升级,而不会编辑pubspec.yaml或 lockfile,但仍会运行 Dart/提取器检查,并可能更新工具缓存或使用临时文件。generate_from_contract.py也会将依赖修改排除在事务性产物计划之外,因此在运行该命令前要显式运行ensure_fr_acdd.py。将提取器预检或依赖不兼容视为硬性失败。该命令只会刷新前端所有的 BFF 内容,并保留后端区段。--check会确认所引用的 SDK 适配器类,但不会将.srv.dart与生成的模板进行比较。 - 最终验证要求每个已声明的 Dart part 都存在,拒绝生成的
.v和适用的.vm存根标记,并且在@FrState/@FrStateJson启用 JSON 生成时要求存在.freezed.dart和.g.dart。 - 使用
@FrState/@FrStateJsonFreezed 模型;将 Model/View 助手和 Event 处理器放在.vm.dart中。 @FrState和@FrStateJson都要求进行 JSON 代码生成,因为两个预设都启用了toJson。在part 'xxx.freezed.dart';旁声明part 'xxx.g.dart';。在拥有该 Model 的 package 中,将json_annotation添加为直接运行时依赖,将json_serializable添加为直接开发依赖。绝不可使用--dev添加json_annotation。- 生成的
_$XxxToJson/_$XxxFromJson函数只能存在于生成的.g.dart中。绝不可在.c.dart、.v.dart、.vm.dart或.srv.dart中定义它们。 - 缺少生成的 JSON 函数时,检查所属 package 的
json_annotation/json_serializable依赖和 Shell 的.g.dartpart,然后运行fvm dart run build_runner build。绝不可通过在 VM 或其他源 part 中编写函数来修复生成问题。 - Component SDK 适配器 Service 使用应用所有的
Dio和lib/api/gen中的生成客户端。应用根目录只注册一次共享拦截器;Component Service 绝不可修改该实例或拥有其 base URL。 - 格式化更改过的 Dart 文件;生成的 part 发生更改时运行 build_runner;并运行仓库分析器命令。
Compatibility
acdd_scaffold支持 Android、iOS、macOS、Web、Windows 和 Linux,同时保留 Android+iOS 作为默认平台。只有选择 macOS 时,才会应用 macOS 部署、存储、权限和 Debug 签名配置。- macOS Debug 使用嵌入式、仅用于开发的加密密钥和单独的非沙盒权限,因此本地启动无需使用 Keychain、Apple Team 或个人证书。Profile 和 Release 仍处于沙盒中,并使用 Keychain Sharing;分发前请配置项目自有的身份和签名。切勿将 Debug 存储用于真实敏感数据。
- 适配现有项目时,除非用户明确批准更改,否则会保留项目当前的平台目标、组织标识符、路由、业务行为和平台原生配置。
- 适配是结构性调整,而不是破坏性重新生成:将所需的脚手架职责合并到现有代码中,不要使用
acdd_scaffold.py覆盖项目。 - 现有项目保留当前页面根目录;只有新建的脚手架项目会默认将路由专属组件放在
lib/app/<route-segment>/,将跨路由组件放在lib/components/<component-name>/。路由专属的共享 Widget 默认放在lib/app/<route-segment>/widgets/;跨路由共享 Widget 默认放在lib/widgets/。仅当用户明确请求adapt_project任务时,才会通过已批准的当前路径到目标路径映射,将代码迁移到这些根目录。 - Figma 仅以只读方式用于契约追踪。每个新增或修改的主要契约都会在
.c.dart中记录确切的当前 Frame 名称、可见页面标题和完整的节点专属 URL;每个Figma States条目只记录其node-id。现有Frame/Node契约仍可读取。不会向 Figma 写入任何flowr共享插件数据、卡片、注释或等效契约元数据。 - 为保持兼容性,现有完整 URL 格式的
Figma States条目仍可读取;所有新增或修改的状态声明都只使用node-id。无需兼容性配置。 - Figma 发布配置为可选项。未配置该项的项目保留仅绑定契约的行为。启用该项的项目在
skills-config中只保留全局不可变的发布名称、文件键、状态、明确指定的活动发布以及强制模式;具体的主要 URL 和状态节点 ID 仍仅保存在.c.dart中。 - 契约工作流取代旧版以 JSON 为先的
new_page.py --spec-file和单个xxx_page.dart布局。不提供兼容模式。 - BFF v9 和 library-shell 多端点提取要求
fr_acdd >= 0.7.0。工作流会检查解析后的 Pub 版本,而不仅仅是依赖约束。在可能的情况下,托管依赖会自动升级到^0.7.0;path/git 来源会保留其声明的来源,但其自身解析出的版本必须兼容。 - BFF v9 是一项会造成不兼容变更的前端契约迁移。严格契约/最终验证会拒绝单数形式的 BFF
Behavior:、组件全局请求来源、不完整的端点Behaviors:、缺少Interactions:、v8 产物或缺失的 BFF Service 类。明确采用非 BFFAPI:模式时,仍保留其单数形式的Behavior:语法。BFF Runtime、BFF Service: none和省略 BFF Service 声明的方式均已废弃。重新生成会将元数据/前端领域升级到 v9,同时逐字节保留后端领域。 - 前端
.c.dart契约中不允许使用Backend Calls、Backend Call Flow、SDK Calls和SDK Call Flow。后端开发者在xxx.bff.md的受保护章节后端业务流程与业务逻辑 API中维护相关内容。 - 未配置配置文件时,本地 OpenAPI 引用从项目根目录解析。项目可以配置另一个位于项目内部的检出根目录,但 BFF 包和同步操作绝不会包含本地 OpenAPI 文档;请通过其独立的权威来源发布这些文档。
- 自由文本形式的 Theme 声明属于旧版架构。它们仍可读取,但只会产生明确的迁移警告;刷新、生成和严格验证要求使用上述结构化 Theme 形式之一。现有的
none和material声明保持原有行为。 - 新生成的
@FrAcddPageWidget 使用 Flutter 的实验性package:flutter/widget_previews.dartAPI,因此要求 Flutter 3.35 或更高版本。现有 Widget 不会自动迁移。