Skip to content

[BUG] 平台拒绝有两种信封形状,咽喉只认扁平那种 —— gateway 模型面的稳定码被结构性丢弃 #962

Closed

Description

@jinjunnn

这是什么

#918 / #940 花了两轮把「从平台响应里取稳定分类码」收编成咽喉(platform-error-code.ts),
为的是让用户看到「你要的那条限制,这个执行形态根本强制不了」这类诚实说明,而不是一个 http-400。

但平台的拒绝有两种信封形状,咽喉只认其中一种。

主 session 实读确认(不是转述):

咽喉只读顶层 code —— packages/ui-mac/src/main/platform-error-code.ts:

const body: unknown = JSON.parse(await res.text())
const code = (body as { code?: unknown } | null)?.code
return typeof code === "string" && CLASSIFICATION_CODE.test(code) ? code : fallback

而 gateway 的模型面发的是嵌套形状 —— alpha-platform/packages/gateway/src/worker.ts:

:178  { error: { message: "refusing to serve: PLATFORM_ENV=dev …", code: "dev_semantics_on_production_target" } }  500
:268  { error: { message, code: BILLING_UNREADY_CODE, missing: r.missing } }                                       503
:281  { error: { message: "rate limited: too many requests from this IP", code: "rate_limited" } }                  429
:527  { error: { message, code: "settle_intent_unavailable" } }                                                     503
:894  { error: { message: "forbidden: token is not authorized for model.invoke", code: "action_forbidden" } }       403

⇒ 这些码结构上到不了咽喉:body.code 是 undefined,直接回落 http-429 / http-500 / http-403。

为什么这比「少一个码」更糟

咽喉的注释与 #940 的判据都写着「无 code ⇒ 保持 http-<status>,不猜」——那是一条
fail-closed 的诚实纪律。但在这条路上,码是有的,我们只是没读。
于是下一个人读到 http-429 会以为「平台这次没给码」,而事实是给了,被丢了。

一条 fail-closed 的纪律,被用在一个不该 fail-closed 的地方,就变成了误导。

⚠️ 可达性未实跑 —— 这是修之前必须先补的那一半

形状差异是逐行读出来的、确凿;但「用户真的会经 /v1/models 撞上嵌套码」这一步,
勘破是按中间件注册模式推出来的(worker.ts:291 的 /v1/* rateLimitGate + :185 的 *
envIdentityServeGate),没有实跑——现有测试用 /v1/chat/completions 驱动那只桶。

按本仓纪律「勘破先于闸门设计」「散文断言不算勘破」:先补一条实跑判据证明这条路走得到,
再谈修
。如果实测发现桌面端根本不经那条路撞上嵌套码,那这张票就该收窄或关掉。

现成的实现参考(但别整套搬)

已作废关闭的 PR #956(分支 fix/940-platform-error-code-chokepoint 保留未删)里,
那套平行咽喉 platform-http-error.ts 本来就同时认两种形状
(cloud 顶层 {error,code} 与 models/account 嵌套 {error:{message,code}})。
可以取它的解析形状作参考,但不要把那整套平行咽喉搬回来 —— 现行咽喉是 #955 的
platform-error-code.ts,两套并存是我们刚消灭掉的形态。

边界

packages/ui-mac/src/main/platform-error-code.ts 的解析。
不要改平台侧的信封形状 —— 那是 ap#330 的范畴,而且那张票的勘破已经证明「统一形状/补码」
这个方向的体量约 290 处、且会打破一条用户可达路径。本票只做消费端认两种形状。

退出条件

  • 先勘破:补一条实跑判据,证明桌面端确实会经某条路径拿到嵌套 code(/v1/models 或别的)。
    拿不到就说清楚拿不到,收窄票面。散文断言与「按注册模式推理」都不算。
  • 咽喉同时认两种形状;**fail-closed 那一半不能松**:两种形状都没有合法 code 时,仍然保持 http-`,不猜。
  • 判据落在用户可观察结果上(错误行里出现的是那个码,不是 http-429),不要只断言解析函数的返回值。
  • 新增/改动的判据要能被跑到:注意本票涉及的两个测试文件此前不在 gate-files.tsv 里
    (#940 合并时已登记 alpha-upload.test.ts 与 upload-consent-dialog.test.ts)。
  • 写完自己实施一遍绕过:把嵌套解析去掉,判据必须转红。

复杂度:S~M。来源:ap#330 只读勘破(2026-08-13)+ 主 session 复核。
Refs #918、#940、#955、jinjunnn/alpha-platform#330。

Activity

  1. added
    type:bugSomething is incorrect or regressed
    prio:P2Planned normal-priority work
    area:integrationRepository, API, or external integration
    on Aug 14, 2026
  2. jinjunnn commented on Aug 14, 2026

    @jinjunnn
    OwnerAuthor

    编排者裁决(2026-08-14):票面收窄 + 排在 #965 之后,不在本批实现

    本轮对 Ready 池做了 10 条 lane 的只读勘破,本票的那条把「票面自己点名未做的那一半」补上了。结论有三条,其中两条改变票面。

    1. 形状差异属实,而且是实跑出来的

    同一条拒绝(IP 限流 429),两个 worker 两种信封,同一个咽喉一个认一个不认:

    cloud worker   POST /v1/cloud/jobs  → {"error":"rate limited…","code":"rate_limited"}        httpErrorCode ⇒ "rate_limited"
    gateway worker GET  /v1/models      → {"error":{"message":"rate limited…","code":"rate_limited"}}  httpErrorCode ⇒ "http-429"
    

    码是有的,确实被丢了。这一半不用再查。

    2. ⚠️ 但退出条件在这条路径上结构性写不出来 —— 票面要改

    票面写「判据落在用户可观察结果上(错误行里出现的是那个码)」。那条错误行今天不存在:桌面对 gateway 唯一经咽喉的请求是 alpha-platform-models.ts:46 的 GET /v1/models,它的错误串三个消费点全部断头 —— main/index.ts:735 与 :1317 两处 .catch(() => {}) 丢弃返回值,IPC models-platform-live 自 97f790706(#442/#447 删 hidden-picker)起零 renderer 调用方。三条独立轴查证。

    所以本票不能按原口径验收。可选项:(a) 收窄成解析层 + 如实写明本路径今天无用户出口;(b) 重心挪到 account 面(那里有用户出口);(c) not-planned。裁决:走 (a)+(b) 的组合,但要排序 —— 见下。

    3. 真正值钱的发现不在票面上:先修本票会制造一个用户可见的退步

    #955 把 alpha-account-request.ts 接进咽喉时,丢掉了已作废分支里那条明写的豁免。实跑 accountResultState:

    • http-503 → recovering(正在恢复,继续重试)
    • billing_unready → failed(终局,停止重试)

    account-server 的真实信封正是嵌套的 {error:{message,code}}。⇒ 一旦咽喉认了嵌套形状,一个瞬态的 billing-unready 会被判成终局,composer 停止重试 —— 而 account 面是有用户的那条路径。

    裁决

    1. 本票排在 #965 之后,不在本批实现。#965(model-recovery 的 transient 判定改读结构化 status,删掉从错误字符串抠数字的正则)是本票的安全前置:它一落地,上面第 3 条那颗雷就结构性消失 —— transient 判定不再看字符串形状,咽喉认不认嵌套码就与「要不要继续重试」解耦了。#965 已进 2026-W33,本批在做。
    2. #965 合并后,本票按 (a) 收窄执行:咽喉同时认两种形状,fail-closed 那一半不松(两种形状都没有合法 code 时仍保持 http-<status>,不猜),并在票面如实写明 /v1/models 这条路径今天没有用户出口、判据只能落在解析层 + account 面。
    3. 两票不并行:修复面重叠(platform-error-code.ts / alpha-account-request.ts / model-recovery.ts),同期开工只会互相冲突。

    顺带订正票面三处(勘破实读)

    • 行号全部漂了 2~13 行。
    • 票面列的五个嵌套点里,billing_unready 结构上到不了 /v1/models(readinessGate 的路径枚举不含它);真正可达的只有 2 个。
    • gate-files.tsv 那段两个方向都反了:票面说「已登记」的 alpha-upload.test.ts / upload-consent-dialog.test.ts 其实不在册;而本票真要改的 platform-error-code.test.ts / platform-error-code-gate.test.ts 在册且是精确条数 4/4 —— 加一条用例就红,实现方必须同批更新登记条数。

    勘破全文留在本轮 session 的证据目录;以上每一条都有真实命令与输出,没有散文断言。

  3. jinjunnn commented on Aug 15, 2026

    @jinjunnn
    OwnerAuthor

    订正块(2026-08-14,实现方)—— 原文一律保留,由本块点名覆盖

    按本仓「基线/票面被推翻时原文划掉保留 + 带日期订正块」的规矩。下面每一条都对应上面某一句,不是重写。

    一、退出条件「判据落在用户可观察结果上」——在全部五个消费者上今天都写不出来

    判据落在用户可观察结果上(错误行里出现的是那个码,不是 http-429),不要只断言解析函数的返回值。

    (编排者裁决第 2 节:「重心挪到 account 面(那里有用户出口)」/「判据只能落在解析层 + account 面」)

    account 面也没有出口。 三条独立轴:

    • account-server.ts 的 35 个嵌套信封里带 code 的是 0(唯一带码的 /v1/preauth 是 gateway→account 的服务器间端点,桌面到不了);
    • renderer 三个 api.account.summary 调用点(alpha-composer.tsx / alpha-composer-model.tsx / renderer/sidebar/alpha-sidebar.tsx)全部只读三态,从不渲染 error 字符串(出错时渲染的是 —);
    • account.transactions IPC 零 renderer 调用方。

    三个 cloud 消费者的宿主 cloud.ts 嵌套信封数 = 0(全扁平),本来就工作;唯一真用户出口
    cloud-dispatch-box.tsx 的 dispatchError(code) 走的正是那条扁平面。

    ⇒ 本改动今天带来的用户可观察行为变化是 0 条,在全部五个消费者上。 已按此交付并在 PR 里写明。
    它值得做的理由与 #965 同类:这条依赖会一直生息 —— 平台每加一处 gateway 面的分类码,今天都白加。

    降级如实标注(不假装有闸): 退出条件①(实跑证明桌面会经某条路径拿到嵌套 code)由一次性跨仓探针满足
    (勘破轮导入真 gateway worker,GET /v1/models 限流 ⇒ 真 429 嵌套信封 ⇒ 喂进真咽喉得 http-429)。
    本 PR 未留下持续判据 —— C1~C5 全是本仓内的合成夹具,平台哪天改掉 /v1/models 面的信封形状,本仓不会红。

    二、gate-files.tsv 那段:只反了一个方向,不是两个

    gate-files.tsv 那段两个方向都反了:票面说「已登记」的 alpha-upload.test.ts / upload-consent-dialog.test.ts 其实不在册

    实读 HEAD 4558a09e3 的 scripts/gate-files.tsv:第 191 行登记 alpha-upload.test.ts(精确 8)、
    第 192 行登记 upload-consent-dialog.test.ts(精确 13)——两个都在册,票面括号里那句本来就是对的。

    只有票面另一半是错的:本票要改的 platform-error-code.test.ts(:188)与 platform-error-code-gate.test.ts(:189)
    在册且是精确条数 4/4,#844 起第 1 列是精确条数不是下界 ⇒ 加一条用例就红,必须同批更新登记数。
    本 PR 已把 :188 改成 6,:189 保持 4(该文件未加用例)。

    为什么这条必须订正:照「两个方向都反了」去补登记 = 往登记簿加重复行,而 assert-gate-files.sh 对重复
    (workdir,path) 判红 —— PR 会在一个与本票毫无关系的地方红,而最省事的止红方式是把行改成 >=N 例外,
    一道精确条数闸就此退回下界。

    三、常量名与行号

    :268 { error: { message, code: BILLING_UNREADY_CODE, missing: r.missing } } 503 等五行的行号

    行号今天是 :180 / :269+:270 / :283 / :528+:529 / :907(票面写的是 178/268/281/527/894,漂 2~13 行)。
    BILLING_UNREADY_CODE 的值是 billing_dependencies_unready(readiness.ts),不是票面写的 billing_unready;
    现有 platform-error-code.test.ts 里的 billing_unready 是合成字面量,不对应任何真实平台码(作为独立锚点合法)。

    五个嵌套点里 /v1/models 上可达的只有 2 个:rate_limited(429)与 dev_semantics_on_production_target(500)。
    billing_dependencies_unready 结构上到不了 —— readinessGate 的注册列表不含 /v1/models(实跑证实:prod 依赖全缺时 /v1/models 仍 200)。

    四、「跨仓契约冲突」——查证后不成立,已按 (a) 开工,未改平台侧

    勘破轮曾提出「让咽喉认嵌套 = 让 alpha-platform cloud-jobs-v1.md 当天写下的顶层槽契约变假」,并据此建议改平台侧。
    对抗审计把它推翻了,复核成立:那句话作用域是 cloud schedules 面、主语是 details.code;gateway 面归
    wire-contracts-v1.md(全文无顶层码规则);且 /v1/messages 上嵌套形状是 Anthropic wire 的结构性必需。
    ⇒ 消费端多认一种形状不会让那两句里任何一句变假。本 PR 未触碰 alpha-platform。

    平台侧的对齐建议(两个中间件把码放顶层)与一句文档同步,已另开 jinjunnn/alpha-platform#367,不阻塞本票。
    /v1/models 死错误链路另开 #987(P3 观察票)。

  4. added 2 commits that reference this issue on Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:integrationRepository, API, or external integrationprio:P2Planned normal-priority worktype:bugSomething is incorrect or regressed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions