ProComponents 页面生成与运行时
状态:In progress -- 页面生成器(
internal/dashboard/generator/)与唯一前端运行时(web/src/components/PageRenderer/、SchemaFormRenderer)已落地;真实浏览器 E2E 的 CI 门禁和全部场景验收仍以根目录todo.md为准。
新手入口:本文是实现规范。核心思路、全链路走读与设计取舍的入门讲解见 界面是怎么生成的:核心思路与全链路。
组合模型总纲:本文聚焦生成器与运行时实现。组合方式的心智模型、 React 原语映射、表达力边界与路线见 组合模型与表达力边界。
端到端流程总览
从函数注册到用户操作页面的完整链路(括号内为关键实现位置):
① 注册
SDK / OpenAPI 声明函数,inputSchema 可携带 x-ui-* 呈现 hints
(presentation-hints.md「字段清单与映射」)
├─ registrationguard 拒绝页面级字段进入注册(internal/function/registrationguard)
├─ FunctionContract 落库 function_contracts(internal/model/function_contract.go),
│ schema digest 供 stale 检测
└─ 注册事务边界(M2):事务内只保留权威状态——契约(含 Removed 级联
清理)+ 能力聚合(RebuildResourceCapability);提案与组件模板是
提交后的衍生重建(internal/platform/registry/store.go),失败不回滚
注册,降级为 registration warning(Code 见下)。心跳重注册与手动
POST /api/v1/pages/proposals/rebuild 均可补齐衍生产物。
② 提案生成(确定性:相同输入摘要 + generator version ⇒ 相同 Proposal)
├─ 路径 A:契约落库/实质变更自动触发组件模板重建与提案重算(T2,失败不阻塞
│ 主流程、记 warn + 审计);OpenAPI 上传单请求完成契约落库 + 模板重建 +
│ 提案生成并返回摘要(T4/T5);手动 POST /api/v1/pages/proposals/rebuild
│ (internal/api/page/service.go RebuildAllProposals)退化为兜底入口
└─ 路径 B:组合页编辑器保存 POST /api/v1/versioning/pages/composite
(internal/service/versioning → CreateCompositeProposal)
表单派生全页型同源(internal/dashboard/generator/):
├─ operation/task/report → buildFormPresentation(generator.go)
├─ resource/composite → buildFormFromContract(resource_generator.go)
└─ 字段来源优先级:x-ui-* hints(form_hints.go)> 类型缺省控件
(enum→Select、date→DatePicker、array(enum)→MultiSelect 等)>
schema title > key 人性化
③ 审核发布(发布分级 D5/T10:pages.publishReview 按 env 控制)
├─ required(prod 等;X-Env 缺失从严按 required):
│ ProposalInbox(web/src/components/ProposalInbox)→ accept-and-publish
│ (internal/service/proposal_service.go)
│ ├─ 质量门槛:error 级诊断拒绝发布;blocked/needs_review 需人工处理
│ └─ published_page_specs 不可变快照 + page_versions 历史 + 提案置 accepted
└─ auto(dev 默认):
├─ composite 保存跳过人工接受直接落 published_page_specs
│ (internal/api/page/service.go AutoPublishComposite)
└─ AcceptProposal 落 draft 后自动接续发布(M5;
internal/service/proposal_service.go SetPublishReviewHooks 注入,
回调复用 AutoPublishComposite——已存在草稿走「提案重建草稿 +
发布」)。发布失败不回滚 accept:draft 保留,响应带
publishError 由前端提示走人工链。
两路快照/版本历史不变,error 级诊断仍拒绝发布。
④ 运行时渲染
PageRenderer 按 PageSpec.type 分发(web/src/components/PageRenderer/)
全部表单共用唯一运行时 SchemaFormRenderer(@rjsf/antd + ajv8):
发布页 inline/弹窗表单、编辑器预览(PreviewRuntime)、Invoke 调试页
(web/src/pages/Functions/Invoke)——不允许第二套手写表单实现。
hints 在渲染端二次生效:fields → rjsf uiSchema 仅内存派生,不持久化。
⑤ 受控执行
POST /api/v1/console/pages/:pageKey/bindings/:bindingId/execute
(internal/api/console/handler.go)
├─ 服务端解析 functionId/权限/scope;schema stale 拒绝执行
└─ onSuccessRefresh / refreshOn / 事件绑定(events)完成区块间联动流程约定速查:
| 阶段 | 事实源 | 变更入口 |
|---|---|---|
| 注册 | FunctionContract(含 hints) | SDK/OpenAPI 重新注册 |
| 提案 | PageProposal(generator 产出) | rebuild / 编辑器保存,人工不可直接改 spec |
| 展示 | PageDraft(可选人工调整) | Page Studio,仅限展示类字段 |
| 运行 | PublishedPageSpec(不可变快照) | 只能通过新提案 → 再发布 |
发布分级矩阵(M5 后)
| 入口 | required env | auto env |
|---|---|---|
| composite 保存(CreateCompositePage) | 只建提案 | 保存成功后直接发布(T10) |
| ProposalInbox accept(AcceptProposal) | 只落 draft | 落 draft 后自动接续发布(M5) |
| accept-and-publish(显式) | 直接发布 | 直接发布 |
| 编辑器 SaveDraft | 恒不自动发布 | 恒不自动发布 |
auto 语义统一约束:质量门槛不因免审核降低(error 级诊断照常拒绝发布); 自动发布失败一律不回滚前置成功操作(提案/草稿保留,publishError 带回 前端降级人工链)。
注册衍生重建告警 Code
提案/模板移出注册事务后(M2),衍生重建失败通过 registration warnings 通道暴露(GET /api/v1/functions/warnings,Functions → Warnings 页):
| Code | 含义 |
|---|---|
proposal_rebuild_failed | 提交后提案重建失败(按 resource/function 标识进 Message) |
template_regen_failed | 提交后组件模板重建失败(手动 regenerate 兜底语义不变) |
已知边界:两类告警与现有注册告警同为内存生命周期——进程重启即失、 重建成功不自动清除既有告警条目(按 Count 递增);registration_warnings DB 持久化接线留待独立需求。
同通道另有 SDK 滑动版本门槛告警(规则与高水位语义见 数据流 §4):sdk_version_behind(落后 1 个 minor,放行 +提示升级)、sdk_version_floor_rejected(落后 ≥2 minor 或 ≥1 major,该 进程独占函数不进本次注册)。追赶后 sdk_version_behind 不自动清除,人工 删除兜底。
为什么不集成 React Admin(决策记录)
React Admin 的可借鉴之处是“资源语义 -> 默认后台页面 -> 局部覆盖”,而不是它的 UI 组件或 DataProvider 协议。Croupier 已有 Ant Design Pro/ProComponents,表格、表单、详情、抽屉、步骤、权限和布局能力足够且更贴合现有系统。
| React Admin 概念 | Croupier 对应实现 |
|---|---|
Resource | ResourceCapability + CapabilitySemantics |
getList/getOne/create/update/delete | PublishedPageSpec 的受控 PageBinding |
| List/Edit/Create/Show | ProTable、SchemaFormRenderer、Modal/Drawer、ProDescriptions |
| DataProvider | 服务端 controlled binding execute API |
| 自动路由/菜单 | PublishedPageSpec -> ConsoleMenuSpec -> ProLayout |
不能照搬 React Admin 的 CRUD-only 模型;Croupier 在同一平台中还必须支持 Operation、Task、Report 和审批动作。
生成器职责
生成器从持久化 FunctionContract 与 CapabilitySemantics 生成 PageProposal,且必须是确定性的:相同输入摘要和 generator version 产生相同 Proposal。
生成兜底的显示名使用 humanize 规则:分隔符(. _ - 空格)与 camelCase 边界拆词、每个词首字母大写后以空格连接(如 player.ban → Player Ban、HTTPServer → HTTP Server)。实现位于生成器 HumanizeKey;页面发布后仍可在 Page Studio 覆盖。
生成器同时负责产出默认 NavigationSpec:ResourcePage title 取 humanize resourceKey;Operation/Task/Report 的 title 取主 binding 的 summary[systemDefaultLocale],缺失时 humanize operationKey,再缺失时 humanize 原始 functionId;category.key 对 ResourcePage 取 resourceKey 的第一个 . 前缀、对独立 Operation/Task/Report 页面取原始 functionId 的第一个 . 前缀(无 . 时取完整 key)。T-M8 起 category 只携带该 key,不再产出分类 labels——分类多语言名称由菜单系统(menu_items.labels)提供,存量分类名经 scripts/migrate-categories-to-menus.sql 归位(humanize 结果即迁移前的默认 labels 来源)。不得从 operation--mail.send 这类 pageKey 推断分类。SDK/OpenAPI 注册不得提供 labels。LocalizedText 显示时按当前界面语言、系统默认语言依次回退;生成器只保证系统默认语言。只有能产出系统默认语言 title 的 Proposal 才允许标记为 ready/basic;title 不齐备时必须降级并记录 diagnostic,不得让“可直接发布”的 Proposal 到发布时才失败。
Resource CRUD 模板
当检测到 collection query 与 identity 时生成 ResourcePage;create/update/delete 是可选能力。只有查询能力的资源生成只读 ResourcePage,不得错误降级为 OperationPage:
collection_query -> ListViewSpec -> ProTable
item_query -> DetailViewSpec -> ProDescriptions
create/update -> FormPresentationSpec(CreateForm/UpdateForm) -> Modal / Drawer + SchemaFormRenderer
delete -> ConfirmActionSpec -> Popconfirm
action -> row / toolbar / batch action列表字段、详情字段和表单字段由 JSON Schema 提供候选;生成器必须记录选择理由。ActionSemantic.subject=resource_item、resource_selection、none 分别确定性映射为 row、batch、toolbar action;前两者缺少可验证 identity input 时不得猜测位置,生成独立 OperationPage Proposal,none 不要求 identity input。没有可靠 identity 或 collection 语义时不得伪造 CRUD 页,降级为 OperationPage Proposal。
非 CRUD 模板
| 能力 | 默认页面 | 必须是真实实现 |
|---|---|---|
action / 无 resource 的同步函数 | OperationPage | 表单、确认、受控执行、结构化结果 |
task | TaskPage | 启动、状态、事件、取消/重试、结果 |
report | ReportPage | 查询、数据集、指标/维度、图表或表格 |
approval.required=true | Operation/Task Page | 等待态、approvalId、审批状态,以及审批通过后的同步结果或任务状态;不得显示为完成 |
Page Studio
Page Studio 的第一屏是 Proposal Inbox,而不是 JSON 编辑器:
- 显示“可直接发布 / 需要处理 / 契约变更”三个队列:可直接发布 =
ready+basicProposal;需要处理 =needs_reviewProposal 和 BlockedProposalIssue(只含诊断与修复指引,不携带 spec);契约变更 = source digest 变化导致 stale 的 Draft 和 PublishedPageSpec。 ready/basicProposal 可查看预览、接受、发布。- 编辑 ResourcePage 时提供列表、列、详情、表单、动作、导航和权限面板。
- 编辑 Task/Report 时提供任务/数据集/图表的受控配置面板。
- JSON 只允许导入、导出、诊断和受权限保护的高级模式,不能是默认编辑器。
- 函数或语义变化时展示 Proposal 与 Draft/PublishedPageSpec 的三方 diff;用户选择合并或修复后发布。
PageSpec 节点与运行时组件
PageSpec 节点到 ProComponents 的对应关系固定如下(renderer adapter 的唯一映射表):
| PageSpec 概念 | 运行时实现 |
|---|---|
ListViewSpec | ProTable,包含筛选、分页、列设置、批量选择和 toolbar |
DetailViewSpec | ProDescriptions / Descriptions |
FormPresentationSpec(create/update/operation/task/report 查询表单) | SchemaFormRenderer,基于 @rjsf/antd + @rjsf/validator-ajv8 |
ConfirmActionSpec | Popconfirm / Modal.confirm + 后端风险与审批策略 |
ActionSpec(row/batch/toolbar) | 表格行内按钮 / 批量栏 / 工具栏按钮,经 binding execute |
TaskViewSpec | 真实 Task binding(status/events/result/cancel)的状态、事件和结果视图 |
DatasetSpec + ChartSpec[] | @ant-design/charts 或等价 AntV renderer;表格用 ProTable |
ResultViewSpec | 结构化结果区(键值/集合/标量),禁止原始 JSON 直出 |
ConsoleMenuSpec | ProLayout 的动态左侧菜单 |
运行时约束
- ProLayout 只合并固定系统菜单和
ConsoleMenuSpec,不从 Resource/Function 推断运行菜单。 - 每个 Page renderer 只消费 PublishedPageSpec,不读取草稿或最新 descriptor 来补字段。
- 每次调用只走 binding execute API;运行时状态按 page instance 隔离。
TaskView连接真实 Task API;ReportView使用真实图表 renderer;禁止 JSON 占位进入发布态。- 所有 loading/error/empty/permission/stale 状态由统一 renderer shell 处理,避免每个页面重新实现。
表单渲染
Function input、查询、创建、编辑、动作弹窗(含组合页 dialog 区块)、编辑器 预览(PreviewRuntime)和 Invoke 调试页共用 SchemaFormRenderer。表单 runtime 固定为 @rjsf/antd + @rjsf/validator-ajv8:
JSON Schema + FormPresentationSpec
-> SchemaFormRenderer
-> rjsf uiSchema derived in memory
-> JSON Schema validation
-> typed PageBinding input assignments这层不得耦合 PageSpec 的页面布局。renderer 私有 uiSchema 只能由 FormPresentationSpec 在前端临时派生,不能持久化、不能进入 SDK/OpenAPI、不能成为第二套页面协议。项目内禁止保留 Formily、form-render 或自行开发 ProForm field factory 作为并行运行时;同样禁止在渲染路径上手写原生 <input> 表单(如旧版弹窗/预览实现,已于 2026-09 移除,统一走 SchemaFormRenderer)。
真实验收
以下项目必须由真实浏览器 E2E(web/e2e/,real-dashboard Playwright 项目)验证,回归入口见 真实 Dashboard E2E:
- OpenAPI REST 和 SDK capability 各生成一个 ResourcePage,并可发布执行;覆盖完整 CRUD(
@openapi-*)与 SDK 显式资源(@sdk-*)。 - 无 CRUD 语义函数生成
basicOperationPage,并可直接发布。 - 任务页能持续显示真实状态和事件。
- 报表页渲染真实图表或表格数据。
- 函数变更后页面被 stale 阻断,用户完成 diff、合并和重新发布(
@schema-change-*、@stale-*、@safe-auto-merge、@republish-*)。 - 切换 game/env 后菜单、页面和执行严格隔离。
