Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

console-ai 付费墙时刻三处断裂:「升级方案」跳到裸 JSON 404、确认卡无失败态、输入框回填旧提示词 #7253

Closed

Description

@hotlong

现象(付费墙时刻的三处断裂)

2026-09-02 最新主干(objectui pin 7a5da141 / cloud 3856fbf7)本地 rig,免费用户在 AI 构建第 2 次迭代点「确认修改」时撞上日额度(cloud 侧 AI_DESIGN_QUOTA_EXHAUSTED 429)。console-ai 的反应:

  1. 「升级方案」按钮是死链:点击跳到 http://<cloud-host>/apps/cloud-control/sys_environment,返回裸 JSON {"success":false,"error":{"code":"ENDPOINT_NOT_FOUND","message":"Not found"}}。缺 /_console/ 前缀,应用 id 也不是控制面里真实存在的(cloud 侧定价页在 /_console/apps/com.objectstack.cloud/page/pricing,套餐入口在 cloud 控制台左栏「计费」)。来源:packages/app-shell/src/console/marketplace/marketplaceApi.ts:433(420 行同款拼法指向 sys_package,请一并核对)。
  2. 确认卡没有失败态:被 429 拦下后,「Create metadata — Awaiting Approval」卡仍停在「确认修改 / 调整方案」,看不出刚才那一下失败了。
  3. 输入框被回填上一条提示词:拦截后 composer 里又出现了用户上一条完整消息,像是要让用户重发。

聊天区那张「需要升级 — 今天的 AI 设计额度已用完,明天会重置;升级后可继续设计」卡片本身文案是好的,问题是它的出口和它周围的状态。

修复要求

  1. 升级按钮指向控制面真实的定价/套餐页;从环境宿主跳控制面时用 cloud 下发的 URL(不要在客户端拼 /apps/cloud-control/...)。
  2. 被额度/权限拦下时,确认卡进入显式失败态并给出下一步(升级 / 明天再来),⛔ 不回填提示词。
  3. 补一条测试:模拟 429 AI_DESIGN_QUOTA_EXHAUSTED 时的卡片状态与按钮 href。

关联:objectui#2458 第 8 条「免费额度撞墙无预告」。

Activity

  1. self-assigned this
    on Sep 2, 2026
  2. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 1 (epic objectstack-ai/cloud#1841)
    Session: session_c5c0ce54-bb9c-478c-9e5b-cf44b80d4569
    Branch: claude/issue-7253-paywall-upgrade-link-and-failure-state
    Worktree: objectui-issue-7253
    Domain: domain:ui
    File surface: packages/app-shell/src/console/marketplace/** (upgrade href), console-ai quota/429 handling under packages/app-shell/src/console/** (confirm-card failure state, composer refill) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (PM judgement; objectui has no dispatch-gates)
    Clause-②: no
    Serial constraints cleared: only dependabot version-bump PRs touch packages/app-shell (package.json) — no code overlap; sibling #7254 (studio + i18n dicts) and #7257 (metadata-events hook) are disjoint files, stop and report if they collide

  3. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    Claim: dev
    Session: session_c5c0ce54-bb9c-478c-9e5b-cf44b80d4569
    Branch: claude/issue-7253-paywall-upgrade-link-and-failure-state
    Worktree: objectui-issue-7253
    File surface: packages/app-shell/src/console/marketplace/{marketplaceApi.ts,MarketplacePackagePage.tsx} + its __tests__ mocks, packages/app-shell/src/console/ai/AiChatPage.tsx, packages/app-shell/src/layout/AiUsageIndicator.tsx (+ its test), packages/plugin-chatbot/src/ChatbotEnhanced.tsx, two new test files, one changeset.
    Bounded in-place fix declared: cloudInstallDeepLink (the issue's "420 行同款拼法") is the same dead composition in the same helper and is repaired in the same change, not just checked.
    No collision with #7254 (studio + i18n dicts — this adds NO i18n keys) or #7257 (metadata-events hook).

  4. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 7253,
      "status": "done",
      "branch": "claude/issue-7253-paywall-upgrade-link-and-failure-state",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7273",
      "premise_still_valid": true,
      "summary": "All three breaks confirmed on current main and fixed. (1) Upgrade link: `cloudPricingDeepLink` and its twin `cloudInstallDeepLink` both composed `${cloudBase}/apps/cloud-control/...`, guessing the control plane's console mount (`/_console/`), app slug (`cloud_control`, not `cloud-control`) and route — all three wrong, hence the ENDPOINT_NOT_FOUND JSON. Per the ruling I searched for a cloud-supplied URL and found none: `/api/v1/runtime/config` publishes only `cloudUrl`, `/api/v1/auth/config` is the auth payload, and the 429 envelope carries messageEn/upgrade/topUp/resetsTonight and no URL. So both helpers collapse into `cloudConsoleUrl()` — the runtime-supplied origin with NO path appended (the console mount's own `/` redirect lands the user in the Cloud app whose nav carries Billing) — and the producer half is proposed as objectstack-ai/cloud#1850. The hardcoded `https://cloud.objectos.app` fallback is deleted: no upstream cloud now means no link rendered, at all three call sites. (2) A quota refusal now parks the confirm card on an explicit blocked state with the server's own next step plus the upgrade action, ranked below the three server verdicts; transient failures keep the rollback, because retrying is right there. (3) The composer refill was the GENERIC path, as the PM hypothesis suspected: card buttons send canned text and never updated `lastSubmittedRef`, so a failed card send restored the user's earlier, already-answered prompt. Fixed at the general behaviour (only composer-typed text is restorable), not by branching on the error code. No new i18n keys — the card reuses `chatbotQuota.*` through a helper the composer banner now shares.",
      "tests": "All at 2d5e311ac. NEW: packages/plugin-chatbot/src/__tests__/ChatbotEnhanced.quotaBlockedConfirm.test.tsx (5) and packages/app-shell/src/console/marketplace/__tests__/cloudConsoleUrl-7253.test.ts (4). ABLATION 1 — ChatbotEnhanced.tsx checked out from origin/main; mutation proven on disk (HEAD blob e30b72c26 vs mutated c70e9f59d, markers `quotaBlockedChanges` and `QuotaBlockedNextStep` each grep-counted 0); result `Tests 4 failed | 1 passed` — the 1 green is the unchanged-behaviour control (typed text still restored), the predicted direction; restore proven byte-identical (post-restore hash-object == e30b72c26, `git diff HEAD --stat` empty). No rebuild needed: the test imports `../ChatbotEnhanced` as source in the same package, not through a dist `exports` entry. ABLATION 2 — `cloudConsoleUrl` mutated to re-append the old path (deleted-anchor count 1 to 0, injected marker count 1, blob ff1ef2e42 to 9ad552805); `Tests 4 failed (4)`; restore byte-identical. GREEN: `pnpm exec vitest run packages/app-shell/src/console/marketplace/ packages/app-shell/src/layout/ packages/plugin-chatbot/` -> `Test Files 72 passed / Tests 672 passed`. `pnpm exec vitest run packages/app-shell/` -> `Test Files 1 failed | 596 passed`, `Tests 7 failed | 5786 passed`; the failure is anonSeedScope-5746.enumeration.test.tsx and is NOT mine — with all five touched source files checked out at origin/main the same 7 fail identically, and they pass on Node 22.22.3 (CI's pin) while failing on this container's Node 26.7.0. `pnpm --filter @object-ui/app-shell type-check` and `--filter @object-ui/plugin-chatbot type-check`: both exit 0 with the script name echoed (objectui spells it `type-check`). Builds: app-shell `dist completeness: 1 package(s) complete (898 emitted files verified)`, plugin-chatbot `built in 7.27s`. Gates, quoting their own verdict lines: check-vi-mock-specifiers OK, check-vi-mock-inherit OK, check-control-bytes OK (5999 files), check:i18n-keys OK, check-changeset-presence `11 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)`, check-changeset-no-major `No changeset declares a major bump`. DECLARED NARROWING (lint): both changed packages linted in full by their own `eslint .` — app-shell 1046 files 0 errors, plugin-chatbot 62 files 0 errors, counts read from `--format json`; untouched packages not linted locally, which is safe because eslint.config.js sets no `parserOptions.project` (no type-aware linting) so this diff cannot move a verdict on a file it does not contain. NOT MEASURED: check:readme-exports needs every package built — after building my two it reports zero unjudged self-imports for them; the 61 remaining are other packages' unbuilt dist, a prerequisite of my local run, not a finding.",
      "mcp_calls": "0 — all GitHub reads and writes went through repo-scoped REST via gh (probe: `repos/objectstack-ai/objectui` answered), including two targeted dedupe searches; the zero-result one is a reading because the other returned 2 hits in the same session.",
      "open_questions": [
        {
          "question": "Where should the control plane publish the tenant SPA's upgrade/billing entry point? objectui now names only the cloud ORIGIN, which is one click short of the Billing page.",
          "options": [
            "A - an absolute URL on /api/v1/runtime/config (framework @objectstack/cloud-connection declares the optional key, cloud's subclass fills it). Available before first paint, so it also serves the AI usage ring's pre-wall CTA, and could retire the third dialect the env-lifecycle 402/403s emit as relative `upgrade_url: /settings/billing`.",
            "B - carry it in the 429 envelope's error.details beside upgrade/topUp/resetsTonight. Narrower: speaks only for that one refusal, nothing before the wall.",
            "C - leave it at the origin permanently and accept the extra click."
          ],
          "recommendation": "A, filed as objectstack-ai/cloud#1850. Long-term soundness leads: the console mount, app slug and page name are properties of the CONTROL PLANE's deployment, so any consumer-side derivation is a guess that goes stale silently — the control plane's own two call sites already spell this URL two different ways (`/_console/apps/cloud_control/page/...` in billing.ts vs `/_console/apps/com.objectstack.cloud/page/welcome` in environment-lifecycle.ts:2508), which is the drift proving derivation cannot work. It also matches the pattern this stack already uses, cloud page metadata declaring `environmentsReoute`-style routes to objectui rather than objectui hardcoding them. Real business need is measured, not speculative: this is the free-to-paid conversion click. Error-proofing favours A because absence stays legible (no key means no link, which objectui already renders correctly) rather than a consumer-side default that silently points somewhere wrong. Scope discipline is respected — one optional declared key on a payload the SPA already fetches, no new endpoint; B would add a fourth dialect for the same fact. Naming and shape are the maintainer's call; objectui will read whatever is declared and needs no further change to keep degrading to no-link."
        }
      ],
      "out_of_scope_findings": [
        "filed as #7271: the 7 anonSeedScope-5746 enumeration assertions all go red on Node 26.7.0 (green on CI's Node 22.22.3) — the localStorage write probe, including the file's own S0 counter-probe, observes zero writes; the risk is that S1..S6 are exactly the assertions that read as vacuously passing once the probe stops seeing writes.",
        "filed as objectstack-ai/cloud#1850: the producer half of this card — no cloud-declared upgrade/billing URL reaches the tenant SPA, and the control plane's own two call sites spell that URL differently."
      ]
    }
  5. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    ACCEPT — reviewer of record: PM (epic objectstack-ai/cloud#1841).

    • PR fix(app-shell,plugin-chatbot): repair the three breaks at the AI paywall moment #7273: draft → main, Fixes #7253, 14 files. Surface breach declared and justified: the confirm card lives in packages/plugin-chatbot (ChatbotEnhanced) and the pre-wall CTA in app-shell/src/layout/AiUsageIndicator.tsx; no sibling claim owns either — accepted.
    • Ruling check: no client-side path composition remains — both deep-link helpers collapse into cloudConsoleUrl() (runtime-supplied origin, no path), and the hardcoded staging-host fallback is deleted (no upstream cloud ⇒ no link). Quota refusal parks the confirm card in an explicit blocked state with the server's next step + upgrade action; composer refill fixed at the generic cause (card-button sends never updated lastSubmittedRef), not by error-code branching.
    • Two on-disk ablations with byte-identical restores; 72 files / 672 tests green in the touched areas; type-check echoed for both packages; i18n-keys / changeset / control-bytes gates OK; lint narrowing declared with the eslint config evidence. The 7 anonSeedScope failures are pre-existing on Node 26 (filed finding(test-env): the 7 anonSeedScope-5746 enumeration assertions all go RED on Node 26 — the localStorage probe sees zero writes #7271, not this diff). CI 28 SUCCESS / 3 SKIPPED.
    • Producer half objectstack-ai/cloud#1850 — maintainer ruled A today; handled on that card.

    Flipping ready and enqueuing.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions