Skip to content

fix(console): settings 客户端跟进 framework#3843 响应信封,配置板块整体恢复 (#3366) - #3372

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-3366-settings-envelope-unwrap
Aug 5, 2026
Merged

yinlianghui merged 2 commits into
mainfrom
claude/issue-3366-settings-envelope-unwrap

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3366

问题

framework#3843 把 service-settings 的 5 个响应全部搬进平台声明信封 { success, data },其 changelog 明说了 raw fetch 调用方要付的代价:必须加一跳 body.data。console 的 settings 客户端正是 raw fetch 调用方,没跟进,于是把信封本身当 payload 交给了视图:

  • 11 个命名空间页在 Object.entries(payload.values) 上抛 Cannot convert undefined or null to object,整页落到错误边界;
  • 「全部设置」列表页的 r.manifests ?? [] 把同一个坏 body 变成假空态「尚未注册任何设置」—— 比白屏更误导,会把排查方向带去「插件没注册」;
  • 保存回读 merge 到空,成功 toast 之下值还是旧的;
  • 动作(POST)的 verdict 不在客户端寻找的位置,message / severity 丢失,toast 只剩 HTTP status text。

前提已对 origin/main @ 68b6a28 复核成立(api.ts 的 jsonOrThrow 直接 return res.json();SettingsView.tsx:97 直取 payload.values),服务端一侧也已核对:settings-routes.ts 5 条路径全部走 sendOk / sendError。

改法

生产代码只动 apps/console/src/pages/settings/api.ts 一个文件 —— 视图未改,客户端修好后 SettingsView / SettingsHub 原样工作:

  1. jsonOrThrow 统一解包,判定与 ObjectStackClient.unwrapResponse 逐字相同:success 是 boolean 且 存在 data。两个条件都要,正是错误信封不被解包的原因 —— 失败体是 { success: false, error },没有 data,原样交回,err.payload.error 继续喂 objectstack#4224 的 locked key 与逐字段拒绝渲染。没有 boolean success 的 body 是 fix(app-shell,components): 服务端声明动作的 visible 门按「已声明」判定,visible: false 不再渲染 (#3835) #3843 之前的服务端,原样透传。
  2. runSettingsAction 两臂各读各的:成功臂从 data 取 verdict,上报失败臂从 error.details 取 —— 路由正是把整个 SettingsActionResult 放在那里,好让 message / severity / details 熬过 400。
  3. 每个端点断言自己承诺的形状,读不了就抛具名 Malformed response from …,而不是把坏 body 继续往下传。两个症状本质都是「读不了的 body 冒充读得了的往下走」;两个视图本来就有 error 态,具名错误落得进去。

契约优先的说明:这里的两代兼容不是在消费者侧加宽容别名,而是复用平台客户端自己的同一条判定 —— console 是带版本的静态资产,会跨服务端版本存活。旧形状分支被测试逐条钉住:等最老支持的服务端都带上 #3843,删掉它是一条红测试点名它在替哪代服务端兜底,而不是静默回归。

测试

新增 api.envelope.test.ts(19 例,单元)与 __tests__/SettingsView.envelope.test.tsx(5 例,过真实 ./api + stub fetch 的两屏集成)。覆盖 PM 点名的三面:信封形正确解包、旧裸形仍兼容、错误响应(success:false / 非 2xx)不被解包逻辑吞掉。

apps/console: vitest run src/pages/settings   → Test Files 4 passed (4) | Tests 33 passed (33)
apps/console: vitest run (全量)               → Test Files 22 passed (22) | Tests 208 passed (208)
apps/console: tsc --noEmit                    → 0 errors
eslint src/pages/settings                     → 0 errors(10 条 warning 全部在未改动的既有行上)
scripts/check-changeset-{fixed,no-major}.mjs  → 均通过

反向验证(方向在跑之前先定:普通的「红」方向)。只把 jsonOrThrow 里那一跳解包退回 return res.json():

Test Files  2 failed | 2 passed (4)
     Tests  8 failed | 25 passed (33)

8 条红全部是喂信封形 body 的用例(list / namespace / save 三处解包 + 两屏渲染),裸形、错误路径、动作三组 25 条保持绿 —— 动作那组本来就走自己的解包路径,不受这一跳影响,与预测一致。

再把形状断言一并停用(还原成本次修改之前的真实状态),集成测试复现的是 issue 里那条一模一样的浏览器崩溃栈:

TypeError: Cannot convert undefined or null to object
    at .../apps/console/src/pages/settings/SettingsView.tsx:97:33

也就是说这两个测试文件钉的是真 bug,不是绕着修改写的同义反复。

影响面清点

这个客户端的消费半径是封闭的:仅 SettingsView / SettingsHub / 同目录测试 import 它,仓内没有第二处 raw fetch 读 /api/settings。顺手核对了 console 另外两处直读顶层 body 的 raw fetch(/auth/me/localization、/forms/:slug),服务端确认这两条路由本就返回裸 body(未走 sendOk),不属同一族问题,无需改动、也无 finding 可报。

用户可见,已附 changeset(@object-ui/console: patch;按仓库约定不声明 major)。


Generated by Claude Code

…ope (#3366)

All five `/api/settings` responses moved into the platform's declared
`{ success, data }` envelope; the console's settings client is a raw
`fetch` caller and never took the `body.data` hop, so it handed the
envelope itself to the views. Every namespace page threw
`Cannot convert undefined or null to object` out of
`Object.entries(payload.values)` and went to the error boundary, while
the hub's `manifests ?? []` turned the same body into a false "no
settings are registered" empty state.

- `jsonOrThrow` unwraps with the exact `ObjectStackClient.unwrapResponse`
  predicate (boolean `success` AND `data` present). Requiring both is
  what leaves error envelopes intact, so `err.payload.error` still feeds
  the locked-key and per-field rejection rendering (objectstack#4224); a
  body with no boolean `success` is a pre-#3843 server and passes through.
- `runSettingsAction` reads the verdict from `data` on the success arm and
  from `error.details` on the reported-failure arm, where the route parks
  the whole result so `message`/`severity`/`details` survive the 400.
- Each endpoint asserts the shape it promises and throws a named
  `Malformed response from …` instead of passing a wrong-shaped body on —
  both views already render an error state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 5, 2026 10:22am

Request Review

… the compat branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-Ct8fZq-3.js —
Status PASS —

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 477.09KB 104.71KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 227.16KB 55.71KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 60.83KB 17.25KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.01KB 28.86KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 231.11KB 56.98KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.25KB 39.55KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 105.02KB 25.36KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.55KB 10.59KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants