Account quota top-up action
账号欠费充值操作
A provider quota failure can mean an exhausted first-party account balance, an API-key account problem, or a custom gateway response. All require a useful explanation, but only the first case can be repaired by opening the signed-in user's embedded DeepSeek recharge page. Inferring that case from provider prose or current UI configuration would make historical failures unstable and could expose an irrelevant billing
English
Problem
A provider quota failure can mean an exhausted first-party account balance, an API-key account problem, or a custom gateway response. All require a useful explanation, but only the first case can be repaired by opening the signed-in user's embedded DeepSeek recharge page. Inferring that case from provider prose or current UI configuration would make historical failures unstable and could expose an irrelevant billing action.
Decision
The llm-deepseek-account plugin classifies quota failures for its own route. The credential callback it hands the shared Messages transport releases the account token only for the configured inference origin, and the plugin rewrites the provider-neutral QUOTA failure to ACCOUNT_QUOTA for both HTTP 402 responses and in-band SSE errors. The API-key plugin registers deepseek-official independently and keeps QUOTA; neither route borrows the other's credentials, and a separately configured API key does not affect account-route quota actions.
A quota failure reaches the transcript and the frame-wide notice as two separate facts. The durable terminal-error record keeps the request-time code and always renders the same provider-neutral quota copy, so the Chat transcript, history paging, and the Session log do not depend on current credentials. A newly appended QUOTA or ACCOUNT_QUOTA failure in a Session this Client has bound and materialized also publishes one frame-wide notice; the newest notice replaces the previous one, and history replacement, pagination, and Sessions the Client never opened publish none.
Chat owns the notice host and registers it into the frame-wide shell.overlay slot, so a notice outlives the Chat panel that reported it. The host offers the one live notice, with code, message, dismiss, and keepOpen, to its shell.quota-notice chain child, and renders a generic warning Toast when no entry claims the code. The account package claims ACCOUNT_QUOTA only, showing a Cancel/Top up Modal; without the shared host entry, or with a snapshot that reports no stored credential or a failed account stream, the entry renders the neutral Toast itself, and with that host entry it waits for the first account snapshot instead of rendering a Toast that its own timer could discard. The plugin keeps one shared PlatformPages request channel and one shell.overlay host entry for the Desktop host's single native view: the Account settings page, the quota notice, and the Desktop onboarding recharge page request a page there instead of mounting their own container, the latest request retires the previous owner before the newer page shows, the shared host only closes the page it is given, and each owner decides what its own return re-reads: returning from a top-up page on the Account settings page or the quota notice re-reads profile, balance, and unnotified bonus, while the onboarding recharge return re-reads profile and balance only. Superseding a page, releasing a request, plugin unload, and leaving usage read nothing, and a release arriving after a newer request or a dismissal clears nothing. Back restores focus before any deferred Modal takes it. While a page requested through that shared channel is showing, the native view covers the document, so the entry renders neither Modal nor Toast. A quota failure that arrives while a Settings-opened page is already showing is not retained: it stays subject to newest-notice replacement, and the Modal appears after the viewer returns while that notice is still current. That Modal reports the failure of the original request rather than the account's current balance: the top-up re-read is a separate read, and returning neither proves a successful payment nor dismisses the notice by itself. The notice's own Top up is the retained case: it retains the notice with keepOpen and requests the top-up page; keepOpen returns an identity-safe release, and the entry releases both the hold and its page request when it unmounts, so a claim cannot outlive the entry that took it. Opening the top-up page therefore keeps the notice and the page mounted through later quota failures, which are dropped rather than queued while their persistent failure rows still render. Dismissal or sign-out releases the hold and the page request, so the next live quota event publishes again. Chat's subscription to a Session binding's live events ends with that binding or with the plugin, whichever goes first.
Credentials and billing URLs never enter Chat props or the Session log. Dismissing the notice does not retry the request; signing out closes an open account Modal or embedded page.
Alternatives considered
Derive eligibility entirely in the renderer. Current endpoint, key, and login state may differ from the facts of a historical failed request. It would also couple Chat to credentials and provider configuration rather than keeping only a stable code at the presentation boundary.
Parse the provider error message in the UI. Provider prose is not a stable contract, cannot distinguish API-key and account-token billing, and would duplicate adapter error classification.
Offer recharge for every quota failure. This would send API-key and custom-gateway users to unrelated first-party billing. The generic notice remains useful without claiming a repair path.
Infer billing from global credentials. A configured API key may belong to another account and may be unused by the failed request. The explicit provider route owns credential selection and quota classification.
Consequences
The Session log distinguishes account balance failure from generic quota exhaustion while the transcript renders both with the same neutral copy. One notice serves the whole app, so it stays visible after the reporting Chat panel closes, and a newer notice supersedes an older one instead of stacking. Because the account package claims the notice through a chain child, Web and signed-out users see the neutral Toast and no billing affordance, and the account package never imports Chat. A notice retained by Top up keeps that entry's page mounted through later quota failures at the cost of dropping those notices; the persistent failure rows remain their record. One shared request channel serves both the Account settings page and the notice, so a newer request replaces the visible page while identity-checked releases keep a superseded owner from closing it.
Provider tests cover routing with simultaneous credentials, HTTP 402 classification, and in-band SSE errors; presentation tests cover append-only publication, replacement, dismissal, Toast fallback, and embedded top-up navigation.
中文
Problem
提供方的额度错误可能表示第一方账号余额耗尽、API Key 账号问题,或自定义网关响应。所有情况都需要清晰提示,但只有第一种情况适合打开已登录用户的内嵌 DeepSeek 充值页。若根据提供方文案或当前 UI 配置推断,会导致历史错误随当前状态变化,并可能展示无关的充值操作。
Decision
llm-deepseek-account 插件为本路由分类额度错误:它交给共享 Messages 传输的凭据回调只在配置的推理 origin 上释放账号 token,并把提供方中立的 QUOTA 错误改写为 ACCOUNT_QUOTA,同时覆盖 HTTP 402 响应与流内 SSE 错误。API Key 插件独立注册 deepseek-official 并保留 QUOTA;两条路由不会借用对方的凭据,单独配置的 API Key 也不影响账号路由的欠费操作。
额度错误分别到达消息记录和全应用提示这两条路径。持久化的终止错误记录保留请求时的错误码,并始终渲染同一段提供方中立的欠费文案,因此 Chat 消息记录、历史分页和 Session 日志都不依赖当前凭据。本 Client 已绑定并已物化的 Session 中新追加的 QUOTA 或 ACCOUNT_QUOTA 错误还会发布一条全应用提示;最新提示替换上一条,历史替换、分页以及从未打开的 Session 都不发布提示。
Chat 拥有提示宿主,并将其注册到全应用的 shell.overlay slot,因此提示的生命周期长于报告它的 Chat 面板。宿主把这条实时提示连同 code、message、dismiss、keepOpen 交给 shell.quota-notice chain 子 slot,没有 entry 认领该错误码时渲染通用警告 Toast。账号包只认领 ACCOUNT_QUOTA,显示带取消和去充值按钮的 Modal;缺少共享宿主条目,或快照报告未存储凭证、账号流失败且无快照时,由该条目自行渲染中立文案 Toast,而存在该宿主条目时它会等待首个账号快照,而不是渲染一个可能被自身计时器丢弃的 Toast。插件为 Desktop 宿主唯一的原生视图保留一个共享 PlatformPages 请求通道和一个 shell.overlay 宿主条目:Account 设置页、欠费提示与 Desktop 引导充值页都通过它申请页面,而不再各自挂载容器;最新请求会在新页面显示前退掉上一个持有者,而在更新请求或关闭之后到达的释放不会清除任何内容。共享宿主只关闭它接到的页面,每个持有者自行决定其返回时重读什么:Account 设置页或欠费提示的充值返回会重读资料、余额与未通知赠金,而引导充值返回只重读资料与余额。替换页面、释放请求、插件卸载与从用量页返回均不读取。返回时先恢复焦点,再由任何被延后的 Modal 接管。只要通过该共享请求通道请求的页面正在显示,该原生视图就会盖住本页,因此该条目既不渲染 Modal 也不渲染 Toast。在 Account 设置页已打开页面的情况下新到达的欠费不会被保持:它仍受最新提示替换规则约束,返回后若该提示仍是最新一条就显示 Modal。该 Modal 报告的是原始请求的失败而非账号当前余额:返回时的重读是一次独立读取,返回本身既不证明支付成功,也不会自动撤下该提示。提示自身的去充值属于被保持的情形:它用 keepOpen 保持住提示,并申请 top-up 页面;keepOpen 返回身份安全的释放函数,且该条目在卸载时同时释放保持与页面请求,因此声明不会比取得它的条目活得更久。打开充值页因此会让该提示与其页面在后续欠费期间保持挂载,这些欠费被丢弃而不排队,其持久化失败行仍然渲染。关闭或退出登录会释放保持与页面请求,使下一条实时欠费重新发布。Chat 对某个 Session binding 实时事件的订阅在 binding 或插件先结束的一侧终止。
凭据和计费 URL 不会进入 Chat props 或 Session 日志。关闭提示不会重试请求;退出登录会关闭已打开的账号 Modal 或内嵌页面。
Alternatives considered
完全在 renderer 中推导资格。 当前端点、密钥和登录状态可能已经不同于历史失败请求的事实;这也会让 Chat 依赖凭据和提供方配置,而不是在展示边界只接收稳定错误码。
在 UI 中解析提供方错误文案。 提供方文案不是稳定契约,无法区分 API Key 和账号 token 的计费,同时会重复 adapter 的错误分类。
为所有额度错误提供充值。 这会把 API Key 和自定义网关用户带到无关的第一方计费页面。普通提示已经足够,不应虚构修复路径。
根据全局凭据推断计费。 已配置的 API Key 可能属于其他账号,也可能未被失败请求使用。明确的 provider 路由负责凭据选择和欠费分类。
Consequences
Session 日志区分账号余额失败与普通额度耗尽,消息记录对两者渲染同一段中立文案。一条提示服务整个应用,因此在报告它的 Chat 面板关闭后仍然可见,且新提示取代旧提示而不是叠加。由于账号包通过 chain 子 slot 认领提示,Web 与未登录用户只看到中立 Toast,没有计费入口,账号包也无须导入 Chat。被去充值保持住的提示会让该条目的页面在后续欠费期间继续挂载,代价是丢弃这些提示;持久化失败行仍是它们唯一的记录。一个共享请求通道同时服务 Account 设置页与提示,因此更新的请求会替换可见页面,而身份校验过的释放使被取代的持有者无法关闭它。
提供方测试覆盖双凭据并存时的路由、HTTP 402 分类和流内 SSE 错误;展示测试覆盖仅追加发布、替换、关闭、Toast 回退和内嵌充值页跳转。