Skip to content

fix(actions): forward bodyExtra end-to-end through the action chain - #3924

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6837-bodyextra-forward
Aug 9, 2026
Merged

fix(actions): forward bodyExtra end-to-end through the action chain#3924
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6837-bodyextra-forward

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#6837

背景

bodyExtratype: 'api' 动作的静态请求体。spec 半边(objectstack#6819)已合入:params 只保留「参数定义数组」一个含义,ADR-0087 的 inline-action-api-params-to-body-extra 转换在加载时把旧的对象形 params 页面改写成 bodyExtra

而 objectui 每个动作渲染器都用一份显式转发白名单(刻意如此:没有渲染器读的键不该看起来像接好了),这些名单里都没有 bodyExtra。结果:一个已发布页面通过校验、成功发布,然后在到达 runner 前一跳丢掉 payload,POST 出去是空体。这是发布完整性件。

四个落点的实况

落点实况
packages/components/src/renderers/basic/elements.tsx已改element:button 白名单补 bodyExtra: action.bodyExtra。补完后该名单与 spec 的 InlineActionSchema pick list 逐字段对齐(那份 pick list 正是本白名单所镜像的契约)。同文件只有这一处 execute() 转发。
packages/core/src/actions/ActionRunner.ts — 弃用窗 + 合并顺序已改。派发词说的 :780/790 分叉在实际 main 上是 :840/850,且那处分叉服务于参数收集,与 payload 组装无关 —— runner 里原本完全没有bodyExtra 的地方。所以合并顺序落在 runner 自己组装 body 的位置(executeAPI),弃用告警落在「非数组 params」的判定上。
executeAPI 回退路径已改,但文件不是派发词写的 useConsoleActionRuntime.tsx —— issue 正文点明这一处在 ActionRunner.executeAPI(body = JSON.stringify(action.params || context.data || {}),两个调用点)。useConsoleActionRuntime.tsx 免改:apiHandler 两条分支都已经把 bodyExtra 最后合并(:308-309 绝对 HTTP 分支、:385-386 dataSource 分支),复核后未动一行。
packages/types/src/ui-action.ts已改ActionSchemabodyExtra,类型从 spec 派生而非手抄,形状不会与契约漂移。

具体做了什么

转发白名单(components)。element:button 外,四个声明式动作渲染器(action:button / action:group / action:icon / action:menu)同样漏掉了 bodyExtra,一并补上。这是对派发范围的一次有意扩大,在此明说请复核:理由是落点 ④ 只有这些渲染器才是它的消费者 —— element:buttonreadProps 拿到的是无类型的对象袋,根本用不到 ActionSchema,所以若不补这四处,落点 ④ 就是一个「没有任何渲染器读」的死字段,恰好是 spec 注释里警告的那种失败。改动本身是同一个键、同一跳、零契约决策。action:bar 免改:它把成员动作整体 spread 到子渲染器上,天然带过去(已核验)。

合并顺序(core)。ActionRunner.executeAPI(没有 host 注册 api handler 时走的路径)现在集中组装 body,bodyExtra最后合并,与 spec 对该键的描述("merged last (overrides user params)")以及 console apiHandler 两条分支一致。顺带:数组形 params 不再进 body —— 它是定义列表不是 payload,把 ActionParam 定义当请求体 POST 出去从来不是任何端点想要的。

弃用窗(core)。 非数组 paramstype: 'api' 上继续被当作静态 payload 读一个版本窗口,并在 dev 下 warn-once 地点名 bodyExtra。判定条件与 ADR-0087 转换逐条对齐(Array.isArray 作判别式 + api 类型守卫),因此:

  • 不碰 objectstack#6828:url 型动作上的对象形 params第三义(interpolateTargetparam 作用域、executeUrlparams.newTab),bodyExtra 不是它的替代品,api 守卫正是把这条挡在外面的东西。
  • 过滤掉 host 塞进 params 的内部键(_rowRecord / _selectedIds / recordId),否则几乎每次声明式动作点击都会误报。

告警是 dev-only + warn-once,与既有的 warnOnUnknownActionKeys 同一套约定。响亮的那一半在生产者侧(contract-first):spec 的 params 字段直接拒收对象形并在报错里点名 bodyExtra,转换负责改写存量源 —— 到了 runner 这一层生产者已经说过话,剩下要兜的只是 host 在代码里手搓 ActionDef 的情形,所以 warn 是正确的严重级别。

测试

新增两个文件,pnpm exec vitest run 全绿:

  • packages/core/src/actions/__tests__/ActionRunner.bodyExtra.test.ts(11 个)—— 合并顺序(status 在两边取不同值,反向合并必红)、纯 bodyExtra payload、ApiConfig 分支、无 bodyExtra 时的 back-compat 钉子、数组 params 不进 body;弃用窗的正例 + 四个负例(url 型静默、数组静默、host stash 静默、host stash 旁边带真实作者键时仍告警)。
  • packages/components/src/__tests__/action-bodyExtra-forward.test.tsx(4 个)—— element:button / action:button / action:menu 三个渲染器转发到 runner;以及一条端到端:渲染 → 点击 → 断言真实 fetch 请求体里 bodyExtra 压过同名旧 params 键(不注册自定义 handler,让点击走完白名单 + runner 合并两跳)。

反向验证分三次跑,方向事先各自预测,结果全部吻合:

  1. 撤掉 runner 改动 → 11 个里 7 红;其中三条 stays SILENT 负例依旧绿 —— 因为功能不在时什么都不 warn,not.toMatch 空绿。这一点如实记录,撤销无法验证这三条。
  2. 于是改为放宽守卫(去掉 api 判定 + 去掉 host stash 过滤)→ url 负例与 host stash 负例转红
  3. 再放宽 Array.isArray 那条 → 数组负例也转红。三条负例各由其对应的守卫分支单独钉住,非空绿。
  4. 剥掉三个渲染器的 bodyExtra 转发 → 4 个全红,端到端那条的请求体退回 status: 'draft'(旧 params 未被覆盖),正是预测的样子。

其余验证:

  • pnpm exec vitest run packages/core/ packages/types/99 files / 1909 tests passed(含 actionKeys.pin.test.ts)
  • pnpm exec vitest run packages/components/102 files / 823 tests passed
  • app-shell 相关面 useConsoleActionRuntime.test.tsx + RecordDetailView.headerApiInterpolation.test.tsx47 passed
  • pnpm exec turbo run type-check --concurrency=278/78 successful,零错
  • check-control-bytes.mjs OK;check-changeset-no-major.mjs OK;改动文件控制字节自查零命中
  • eslint 改动文件:0 error

changeset

.changeset/quiet-doors-forward-bodyextra.md,patch 覆盖 @object-ui/components / @object-ui/core / @object-ui/types


Generated by Claude Code

An action's static request body (`bodyExtra`) was dropped one hop before the
ActionRunner. Every action renderer forwards an EXPLICIT whitelist of keys —
deliberately, so a key no renderer honours cannot look wired — and none of them
listed `bodyExtra`. Since @objectstack/spec 17 made it the only way a
`type: 'api'` action can carry a payload (`params` keeps its single meaning as
the parameter DEFINITION array, per the #5777 ruling, direction A), and the
ADR-0087 `inline-action-api-params-to-body-extra` conversion rewrites older
object-form `params` pages onto it at load, a previously-working published page
validated, published, and then POSTed an empty body.
- components: `element:button` forwards `bodyExtra`, which brings its whitelist
to exact parity with spec's `InlineActionSchema` pick list. The four declared-
action renderers (`action:button`, `action:group`, `action:icon`,
`action:menu`) forward it too — they are the consumers the type addition below
exists for. `action:bar` needs no change: it spreads the member action onto its
child renderer.
- core: `ActionRunner.executeAPI` — the path taken when no host registered an
`api` handler — assembles the body with `bodyExtra` merged LAST, matching the
spec's documented semantics and both branches of the console `apiHandler`. An
ARRAY `params` no longer contributes to the body: it is a definition list, not
a payload.
- core: the compat window the #5777 ruling ordered. A non-array `params` on a
`type: 'api'` action is still read as the static payload for one version
window and now says so, naming `bodyExtra`. Scoped to exactly the shape the
ADR-0087 conversion rewrites (`Array.isArray` discriminator + `api` type
guard), so it stays out of #6828's third meaning of object-form `params` on a
url action, and filtered against the host `_rowRecord`/`recordId` stash so it
cannot fire on ordinary declared-action clicks.
- types: `ActionSchema` declares `bodyExtra`, typed off the spec rather than
restated.
`useConsoleActionRuntime`'s `apiHandler` needed no change — it already merged
`bodyExtra` last on both its branches.
Fixesobjectstack-ai/objectstack#6837
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 9, 2026 5:02am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DmQMZ7Eh.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)482.53KB106.37KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)230.97KB56.74KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)117.21KB30.27KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)236.63KB59.02KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.63KB49.66KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

验收通过,含扩围批准(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#6837(v17 收尾件)

实物核验:base 99ba5fbd7(origin/main)→ head 9308d1562,单提交 11 文件。CI 亲读终态:20 检查全 completed、0 失败。

验收要点:

  • 扩围裁定:批准。 action:button/group/icon/menu 四个声明式渲染器一并补 bodyExtra 转发成立 —— ActionSchema.bodyExtra(落点④)的唯一消费者就是这四个渲染器(element:button 走 readProps 无类型对象袋),不补则④是死字段;反向验证 D(剥掉渲染器转发→4 红)钉住了这条腿。action:bar 免改的 spread 论证已核。
  • buildApiRequestBody 语义正确:bodyExtra 最后合并(与 spec 注释、console apiHandler 两分支一致);数组形 params 不再被 POST 成请求体是弃用窗的正确推论(参数定义列表从来不是任何端点想要的 payload),changeset/PR 已如实声明。
  • 弃用窗守卫与 ADR-0087 转换逐字对齐(Array.isArray 判别式 + api 类型守卫),api 守卫天然把 #6828 的 url 型第三义挡在外面,注释里的「Do not widen this guard」提示有档案价值;host-stash 过滤(_ 前缀 + recordId)防告警误报的论证经查成立。
  • 三处派发词偏差(行号漂移、executeAPI 归属 ActionRunner 而非 useConsoleActionRuntime、base sha 更新)dev 均以实测更正并给出证据,useConsoleActionRuntime 免改的两分支行号证据(:308-309/:385-386 均已最后合并)采信。
  • 类型从 SpecAction['bodyExtra'] 派生而非复述,契约不漂移。

范围外发现 objectstack#6938(bodyShape 被全部五个渲染器白名单丢弃)立单规范(先查重、unassigned、仅路由标签),v17 冻结期不派,归后续分诊。

GraphQL 配额未恢复则 undraft+auto-merge 由 PM 随后补发。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 05:38
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit 7e5bb5dAug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os6837-bodyextra-forward branch August 9, 2026 05:39
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui: element:button forwards bodyExtra to the ActionRunner; the object-form params arm becomes a deprecation window (#5777 objectui half)

2 participants

@yinlianghui@claude