deepseek-harness/.agents/notes/implemented/architecture/2026-08-26-models-page-extension-slots.zh.md
Yichen Jiang bf0db65bb0 fix(ui-settings-models): address review — derived key fact, required render seat, spec sync
The provider-card seat's keyConfigured now derives from the reference the
page would use — the profile's apiKeyEnv, or the page's derived
<ROUTE>_API_KEY while the profile names none — so the add-provider draft
agrees with its own editor about an existing conventional credential (the
store joins the derived describe in the same batched call, as
ProviderRow.derivedCredential). ModelsSectionProps makes the renderSlot seat
required so a direct render that forgets it fails to compile; the one such
render in provider-form.client.spec regained a real mount and the test
boilerplate collapsed to renderSlot={() => null}. The extension-slots Agent
Note now states the keyed cell's real override rule (same priority throws,
a different priority shadows), and docs/subsystems/slots.md carries the two
new seats in its hierarchy, both languages.
2026-08-26 17:12:58 +08:00

3.8 KiB
Raw Blame History

Agent Note: Models 页扩展插槽

Status: implemented

English | 中文

Problem

出于提供方服务条款的考虑,pi-ai catalog 的提供方登录(GitHub Copilot、OpenAI 账号)正从产品中移出,改由一个可选的仓库外插件承担。该插件需要把登录按钮与登录过程 UI 放进 Models 页的提供方卡片——用户与提供方相遇的界面——但 ui-settings-models 的卡片由封闭代码渲染:唯一的集成途径是修改本包,外部插件做不到;页面仅有的开放缝隙(settings.section)只能新增一整个独立页面。

Decision

ui-settings-models 在 src/client/slot-contract.ts 声明两个 SlotMap 席位,在其 settings.section 注册中以 children 认领它们,并从 ./client 再导出其类型,使仓库外插件通过 type-only import 即可获得类型合并。

settings.models.provider-card 为 keyed,entryKey = ConfigurableProviderView.settingsNs:以某适配器家族的 settings namespace 注册一次,即可收到该家族的全部卡片——内置 catalog 路由、从目录采纳的行、手工声明的路由一视同仁——而分区从不解释这个 key。键域保持开放字符串空间(不设 keyProps 表),因为手工声明的路由 id 由用户在运行时命名。该席位在每张展示目录行的卡片上分发:已保存行的卡片、其首次运行 setup 形态、以及「添加提供方」草稿卡(其休眠行,实际为 configured: false)——草稿卡正是登录价值最大的时刻:用户刚遇到该提供方、手中还没有密钥。手工声明的草稿卡在保存前没有目录行,不分发。Owner props 携带该行的 ConfigurableProviderView、其 configured 合并结果与已确认的 api-key 凭据状态(keyConfigured,首个消费者用它在已存密钥旁抑制登录入口);更多字段没有现役消费者。

settings.models.footer 为 list 席位,位于行列表与新增控件之后,承载孤儿记录管理这类分区级扩展内容。

没有注册方时两个席位均不渲染,产品页面与之前逐像素一致。

Alternatives considered

**用 list 席位、由注册方自行筛选,替代按键分发。**每个注册方都会在每张卡片上渲染(再返回 null),且两个插件可能在同一家族的卡片里静默交错 UI。按 namespace 分发让每个适配器家族有唯一可问责的扩展所有者、零浪费分发,并完全复用 settings.plugin.item 的配对理由。

**按提供方路由 id 分发。**路由 id 是动态的——手工声明的路由由用户在运行时命名——插件无法先于目标行注册,还得随目录变化反复重注册。

**用 chain 席位整体接管卡片。**没有现役消费者需要替换编辑器;登录界面是加法。接管契约还会让分区布局成为兼容面。将来仍可在不动这两个席位的前提下追加 chain。

**把登录 UI 继续织在 ui-settings-models 里(插件化之前的设计)。**会把服务条款敏感的界面随产品发布,而这正是本扩展点要避免的结果。

Consequences

仓库外插件现在无需改动产品即可把按家族的卡片 UI 集成进 Models 页;llm-pi-ai-oauth 是首个消费者。代价是一份公开契约:ProviderCardExtrasOwnerProps 在 ./client 边界暴露 ConfigurableProviderView,且各分发位点(已保存卡片、setup 形态、新增草稿、footer)成为扩展方依赖的行为。每个适配器家族的 keyed 单元格同一时刻只渲染一个所有者:同一 namespace 下同 priority 的第二次注册会被注册表拒绝,不同 priority 则是刻意的遮蔽(最低 priority 的条目渲染)——这是 slot 套件的标准覆盖通道,绝非静默合并。