Skip to content

fix(app-shell): CloudConnectionPanel 绑定失败先读 error.message,与同文件 getJson 的优先级一致 - #5052

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5028-bind-error-message
Aug 17, 2026
Merged

fix(app-shell): CloudConnectionPanel 绑定失败先读 error.message,与同文件 getJson 的优先级一致#5052
yinlianghui merged 1 commit into
mainfrom
claude/issue-5028-bind-error-message

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5028

poll() 的终止分支原来读 body?.error?.code ?? t('cloudConnection.errors.bindFailed')
所以即使失败信封里已经带着一句人能读的话,渲染出来的仍是机器码。同一文件上方的
getJson:76)早就用 message ?? code ?? error 这条链;本 PR 只把唯一偏离的那个
调用点拉回本文件已有的房规,不是新约定。

上游解锁引用

objectstack-ai/objectstack#9267 已于 2026-08-17 17:59:18Z 经 PR #9369(d2e6b1d67)落 main:
/bind/poll 的失败信封现在带 error.messageDevice authorization failed: expired_token
error.declaredCode(RFC 8628 原拼写),data.pending: false 刻意保留,轮询状态机不受影响。

前提复验读数(基线 dc9d651a2c55c10a9e4d30434e429e5fdc5ae968

卡面钉的两处内容级读数,逐字复核,行号与卡面一致:

位置基线内容
CloudConnectionPanel.tsx:128setPhase({ kind: 'error', message: body?.error?.code ?? t('cloudConnection.errors.bindFailed') });
CloudConnectionPanel.tsx:76const msg = body?.error?.message ?? body?.error?.code ?? body?.error ?? 加上 HTTP ${resp.status} 模板串

但卡面的「症状归因」不成立,这是本 PR 与卡面唯一的实质差异。 卡面预测上游落地后用户会看到
DEVICE_CODE_FAILED,即认为设备授权失败是由 :128 显示的。实测不是:

  • 该信封由 /bind/pollHTTP 400 下发(packages/cloud-connection/src/cloud-connection-plugin.ts
    PR #9369 之前是 pending ? 200 : 400,之后仍是 400);
  • getJson 对任何「非 2xx 且 body 不是 success: true」的响应先抛:75),
    poll() 的 catch(:130)渲染的是 err.message
  • 所以过期/被拒的设备码那句文案,一直由 getJson 那条已经正确的链选出。上游 message 一合并就到了 UI,本仓这一侧其实不欠这个。

那么改动的这一行真正管什么:只有 getJson交还的 body(2xx)且既无 data.pending
又无 data.bound 也无 success: true 才会落到这里。/bind/poll 恰有一个这样的出口 —— 它把控制面
/bind 的应答连状态码一起原样转发(c.json(bindJson, bindResp.status))。控制面若以 200 回
success: false(其侧的信封违规,正是 objectstack#9364 还在清点的那一类),就落在这一支,
而这里过去显示的是码、不是它旁边那句话。

反向验证(方向先书面预判,commit 后变异,git checkout -- 还原)

四个用例,编号见测试文件。注意 PM 派单模板预判的是「新信封用例变红」,而实测新信封用例(① 控制组)
不可能因这一行变红 —— 它从不走这一行;能分辨优先级的只有同时带两半的 ② 号。如实记录:

变异预判实测一致
退回只读 code② 红(渲染 ENVIRONMENT_BIND_FAILED);①③④ 绿Tests 1 failed | 3 passed,失败即 ②,dump 里正是 ENVIRONMENT_BIND_FAILED
优先级写反 code ?? message② 红(码胜出);①③④ 绿Tests 1 failed | 3 passed,同一条、同一断言
还原后全绿Test Files 2 passed / Tests 8 passed

① 号在两次变异下都保持绿,就是「设备授权那句话不由这一行产生」的实测证据 —— 它作为控制组留在
文件里,好让下一个读者不必再去 poll() 里找那段文案。

验证

构建先行:

pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build
→ 全部 Done

测试(仓根,经共享 verify 锁,--maxWorkers=2):

pnpm exec vitest run packages/app-shell/src/console
→ Test Files 50 passed (50) / Tests 362 passed (362)
pnpm exec vitest run \
packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.bindError.test.tsx \
packages/i18n/src/__tests__/cloudConnection-locale-parity.test.ts \
packages/app-shell/src/console/marketplace/__tests__/readApiError.test.ts
→ Test Files 3 passed (3) / Tests 16 passed (16)

类型与门禁:

pnpm exec turbo run type-check --concurrency=2
→ Tasks: 81 successful, 81 total(exit 0)
pnpm exec eslint 改动的两个文件
→ 0 errors(6 warnings 全为基线既有的 no-explicit-any / set-state-in-effect,行号均不在本次改动内)
node scripts/check-control-bytes.mjs
→ OK(scanned 4503 tracked text files)

测试范围说明:app-shell 全量过重,按 packages/app-shell/src/console 目录跑(含本次新增文件),
另按消费半径把 packages/i18n 的 cloudConnection 平价测试与 marketplace 的 readApiError 一并跑过 ——
grep 全仓后,这三处是 cloud-connection 相关的全部测试面。

范围

只这一个调用点 + 一个测试文件 + 一个 changeset。卡面自留的 UX 题(是否一律偏好译文而非任何机器码)
不在本 PR 裁 —— 复验把它变成了一个更具体的 翻译 问题(线上那句话是英文,而面板对同一失败已有译文键
cloudConnection.errors.expired),已另立 finding 单记录(见下方评论),不夹带进本 PR。

….code on bind-poll failure
`poll()`'s terminal branch read `body?.error?.code ?? t('cloudConnection.errors.bindFailed')`,
so a failure body carrying a readable sentence beside its machine code was still
rendered as the code. This file's own `getJson` helper already reads
`message ?? code ?? error`; the one call site that diverged now matches it.
Narrower than issue #5028's premise, which verification moved: `/bind/poll`
serves the terminal device-authorization failure with HTTP 400 (both before and
after objectstack PR #9369), and `getJson` throws on any non-2xx whose body is
not `success: true` — so that text was always chosen by `getJson`, not by this
line. What the changed line governs is the 2xx path: the control plane's `/bind`
answer is forwarded verbatim with its own status, and a 200 that says
`success: false` reaches this branch.
Four cases pin the surface end to end, including a control that records where
the device-authorization text really comes from.
Co-authored-by: Claude <noreply@anthropic.com>
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

正文已重写一次:首版里 pnpm exec vitest run 那行的三个文件参数用尖括号占位,被 GitHub 的正文清洗器当成 HTML 标签吞掉了两个(< 紧跟字母才会中招,<新增测试> 这种 CJK 反而留了下来)。现已改成完整路径逐行列出,读回确认无截断。


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-DuLq94Qs.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
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)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
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)502.19KB112.21KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)234.25KB58.48KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
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)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.12KB60.43KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)122.61KB29.71KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.17KB26.99KB
plugin-map (index.js)18.72KB6.09KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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 (dashboard-filter-alias.js)6.23KB2.74KB
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)3.05KB1.52KB
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

PM 验收:ACCEPT(session session_01GTRjn8xBqp75dk7kFupVRt,objectui 分片 PM,批次 22)

实物核验:merge-base 恰为批次基线 dc9d651a2,3 files +239/−1 与报告一致,单 commit,模型标识 msg/diff 双零,releases 零触碰。CI 亲读:20 个 check-run 全 completed 零失败(代码改动,四个 Test shard 真跑收绿,Type Check 21:05:39 绿)。

验收核心是采信这次前提归因修正(premise_still_valid: false 只针对症状归因,不针对修法):

  • 卡面(与 os#9267 解锁评论)预测「用户将在 :128 看到 DEVICE_CODE_FAILED」—— dev 从 objectstack origin/main 与 d2e6b1d67 的 diff 逐行读出:设备授权终止失败以 HTTP 400 下发,getJson(:75)对非 2xx 先抛,catch(:130)渲染的正是 getJson 里早已正确message ?? code 链 —— 上游 message 一合并就到了 UI,卡面症状不会发生。
  • 被改的 :128 真正管的是 2xx 路径:/bind/poll 原样转发控制面应答,控制面以 200 回 success:false(objectstack#9364 仍在清点的信封违规类)才落此支。修法仍正确、与同文件房规同形、零风险 —— 只是用户可见范围比卡面窄,PR 与 changeset 都如实写明,没有把叙述强套回已死的症状。
  • 反向验证的处理是模板纪律的正确用法:模板预判「新信封用例变红」在这条链上不可能成立,dev 保留该用例为控制组(两次变异下恒绿),这个「不变」本身就是归因修正的实测证据 —— 没有改断言迎合模板。

四用例双向钉(2xx 带 message 优先、无 message 回落 code、全缺 i18n 兜底、400 路径控制组),两次变异均只红预判那一条。消费半径三文件 + console 目录 50 文件全绿,仓根 81/81。

派生物:#5054(服务端察觉的失败读英文、本地计时器察觉的同一失败读译文 —— 双语混杂 UX 题,附 A/B/C)已按 finding 入池待分诊,处置正确。顺带记账两条平台行为进案头:issues 端点会剥正文末尾 footer(PR/评论端点不剥,补法=另发评论);getJson 类先例复核时要连 HTTP 状态码路径一起读,不能只看行内表达式 —— 本次解锁评论的前提复验就差在这一步。

转 ready 并挂 auto-merge(squash)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 21:12
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit ee4f796Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5028-bind-error-message branch August 17, 2026 21:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CloudConnectionPanel shows a machine code on bind failure — read error.message now that /bind/poll sends one

2 participants

@yinlianghui@claude