dsh-settings-plus 设置注册 SDK 契约(docs/sdk-contract.md)
August 14, 2026 · View on GitHub
本文档是 @deepseek-ai/dsh-settings-plus 的开放注册接口契约:其他插件如何通过
src/sdk.ts(导出面 @deepseek-ai/dsh-settings-plus/sdk)在宿主机上注册自己的
自定义设置项(settings namespace)。宿主的设置服务、crawler 自动发现与 UI 渲染都围绕
这些已注册的 namespace 展开;本文档定义插件侧注册的规则、生命周期与去重语义。
配套参考:docs/dsh-plugin-contracts.md(模板级插件契约)、src/crawler.ts
(自动发现枚举面)、src/service.ts(dshSettingsPlus 服务面)。
一、SDK 是什么
src/sdk.ts 是薄 helper,不引入宿主包 @deepseek-ai/dsh-settings——它通过最小本地
契约(与 src/crawler.ts 相同的风格)消费宿主 ctx.settings 的 register 面。
SDK 不提供插件生命周期管理(无 install/enable/disable,那是 plugin-registry 的职责)。
导出面 @deepseek-ai/dsh-settings-plus/sdk(package.json exports 的 ./sdk)包含:
| 导出 | 形态 | 说明 |
|---|---|---|
registerUserSettings(ctx, ns, schema, options?) | 函数 | 实注册;返回显式移除 disposer |
defineSettingsSection(ns, schema, options?) | 函数 | 声明式描述;无副作用,不注册 |
settingsNamespace(value) | 品牌函数 | 命名校验(^[a-z][a-z0-9-]*$),非法即抛 TypeError |
SettingsNamespace / SettingsApplies / SettingsSchemaLike / SettingsRegisterOptionsLike / SettingsScopeLike / SettingsSection | 类型 | 本地契约类型(官方 SettingsScope/SettingsRegisterOptions 的镜像) |
二、快速开始
在插件 apply 中注册一个 namespace,并用 ctx.effect 包裹以实现 fiber 自动回收:
import { Context } from 'cordis'
import z from 'schemastery'
import { registerUserSettings } from '@deepseek-ai/dsh-settings-plus/sdk'
export const name = 'my-plugin'
export const inject = ['settings']
const MySection = z.object({
host: z.string().default('localhost'),
token: z.string().role('secret'),
})
export function apply(ctx: Context) {
// 注册是调用方 fiber 的 effect;用 ctx.effect 包裹,fiber 回收时自动移除
ctx.effect(() => registerUserSettings(ctx, 'my-plugin', MySection, {
applies: 'live',
base: { host: 'default-host' },
}))
}
注册完成后,namespace 立即进入宿主的设置面:crawler 的
ctx.dshSettingsPlus.listNamespaces() 会包含它,配置 UI 自动渲染其 schema。
三、API 清单
registerUserSettings
registerUserSettings<T = unknown>(
ctx: Context,
ns: string,
schema: SettingsSchemaLike,
options?: SettingsRegisterOptionsLike<T>,
): () => void
- 内部等价于
ctx.settings.register(settingsNamespace(ns), schema, { base, applies, validate }), 逐字转发,不做任何改写。 - 返回
() => voiddisposer:显式调用时移除该注册(调用提供dispose钩子的 provider 时移除注册;官方宿主 scope 无移除钩子,此时移除由 fiber 回收完成——见 “生命周期”一节)。disposer 必须显式调用才会触发;SDK 不会在调用方背后自行移除。 - 错误在调用点同步抛出,不做包装:
ns不合命名规范 →TypeError(品牌函数抛出);- 宿主 seam 缺失 →
Error(“requires thesettingsservice”); - namespace 重复 → 宿主
register的 duplicate 错误原样传播(见“去重语义”)。
defineSettingsSection
defineSettingsSection(
ns: string,
schema: SettingsSchemaLike,
options?: SettingsRegisterOptionsLike,
): SettingsSection
声明式描述一个设置分区({ ns, schema, options? }),不产生任何注册副作用。
用于“引用分区而不拥有注册”的场景——例如与自动发现协同:把分区声明作为发现结果的
权威来源引用,或在注册前先声明以便校验命名。ns 同样经过品牌校验。
settingsNamespace
settingsNamespace(value: string): SettingsNamespace
官方命名品牌函数(^[a-z][a-z0-9-]*$)。SDK 内所有 ns 入口都先经过它;插件自己
构造已品牌化命名空间时也用它。非法名(大写、前导连字符、数字开头、空格、下划线等)
立即抛 TypeError。
四、namespace 命名规范
- 命名规则:
^[a-z][a-z0-9-]*$——小写 kebab-case,与插件 short name 一致 (如session、gateway-2、my-settings-ns)。 - 每个 namespace 在宿主内全局唯一;同一 ns 由多个注册方声明是配置错误(见“去重语义”)。
- 命名空间是插件的对外身份:选择稳定、可读、不易与宿主自带 namespace
(如
session)冲突的名字;建议以插件名开头(<plugin>-<area>)。
五、secret 字段规则
- 敏感字段用 schemastery 的
role('secret')声明,如z.string().role('secret')。 - SDK 本身不读取、不接触任何值:它只转发 schema 与选项。secret 语义由宿主设置服务
执行——宿主的
describe({ redactSecrets: true })会剥离role('secret')字段并 枚举其位置(descriptor.secrets);crawler 与 dshSettingsPlus 的所有读取面都已 强制 redact,插件无需自行脱敏,但也不要把非 secret 机制(如明文口令字段)声明成 普通字段。 role('secret')字段仍由宿主的配置 UI 正常渲染编辑(带遮盖与显式提交语义)。
六、生命周期:注册随调用方 fiber 回收
-
宿主的
ctx.settings.register把注册建模为调用方 fiber 的 effect:该 fiber 被 dispose 时,namespace 与它的观察者一并移除(宿主契约,官方实现如此)。 -
SDK 返回的 disposer 是显式移除句柄:必须显式调用才会移除。官方宿主 scope 没有
dispose方法,因此对官方宿主而言显式移除不可通过 seam 完成——移除由 fiber 回收 完成。提供dispose钩子的 provider(含测试替身)会收到该调用。 -
推荐模式(fiber 自动回收):
ctx.effect(() => registerUserSettings(ctx, 'my-plugin', MySection))ctx.effect的 cleanup 即 SDK 返回的 disposer;fiber dispose 时 cleanup 运行, 注册被回收。不要只用裸调用并丢弃 disposer——那意味着注册只能靠宿主 fiber effect 兜底回收,无法在 fiber 存活期间提前移除。
七、去重语义(SDK 显式声明覆盖自动发现)
Metis C-6 的去重结论,诚实表述如下:
- 宿主是唯一仲裁者:
ctx.settings.register对已注册 namespace 立即抛 duplicate 错误。SDK 不做注册表仲裁、不模拟复杂的覆盖合并——重复声明的结果是 响亮报错,而不是静默的 schema 争抢。 - SDK 注册是权威来源:通过 SDK 注册的 namespace 会进入宿主的注册表,因而被
C1 crawler 自动发现(
listNamespaces()枚举宿主注册表)。对自动发现而言, “已注册即被发现”是唯一的真相来源:SDK 注册的 schema/options 就是配置 UI 渲染的 schema/options,不存在另一份自动发现 schema 与之竞争。 - 重复注册 = 配置错误:同一 namespace 被 SDK 与(理论上)自动发现或其他插件 同时注册时,先到者胜、后到者在调用点同步收到宿主的 duplicate 错误并向上传播。 修复方式是移除冲突声明,而不是让 SDK 悄悄覆盖。
- 命名失败提前拦截:不合规范的 ns 在 SDK 层(品牌函数)即抛错,不会进入宿主。
八、与自动发现(crawler)的关系
- C1 crawler(
src/crawler.ts)枚举宿主注册表:任何通过官方机制(含本 SDK)注册的 namespace 都会被ctx.dshSettingsPlus.listNamespaces()自动发现——插件无需额外 登记。 defineSettingsSection的声明式形态与自动发现协同:声明可以先行(校验命名、供 引用),注册仍以registerUserSettings为准;两者用同一命名与 schema 即保持同步。- 发现结果是 redacted 快照:
role('secret')值永不出现,secrets数组枚举其位置。
九、本地契约与宿主边界
- SDK 不 import 宿主包;类型是官方
SettingsScope/SettingsRegisterOptions的本地 镜像(SettingsScopeLike额外带可选dispose钩子,用于显式移除,官方宿主缺席时 文档化降级)。schema 参数为结构化SettingsSchemaLike(schemastery schema 天然 满足)。 - 宿主 seam 缺失或
register不是函数时调用点即抛错(fail loud),与 crawler 的resolveSettings同风格。 - SDK 不提供插件生命周期管理(无 install/enable/disable)——那是 plugin-registry 的职责;本契约只管“设置项的声明、注册、回收”。