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.settingsregister 面。 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 }), 逐字转发,不做任何改写。
  • 返回 () => void disposer:显式调用时移除该注册(调用提供 dispose 钩子的 provider 时移除注册;官方宿主 scope 无移除钩子,此时移除由 fiber 回收完成——见 “生命周期”一节)。disposer 必须显式调用才会触发;SDK 不会在调用方背后自行移除。
  • 错误在调用点同步抛出,不做包装:
    • ns 不合命名规范 → TypeError(品牌函数抛出);
    • 宿主 seam 缺失 → Error(“requires the settings service”);
    • 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 一致 (如 sessiongateway-2my-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 的去重结论,诚实表述如下:

  1. 宿主是唯一仲裁者ctx.settings.register 对已注册 namespace 立即抛 duplicate 错误。SDK 不做注册表仲裁、不模拟复杂的覆盖合并——重复声明的结果是 响亮报错,而不是静默的 schema 争抢。
  2. SDK 注册是权威来源:通过 SDK 注册的 namespace 会进入宿主的注册表,因而被 C1 crawler 自动发现(listNamespaces() 枚举宿主注册表)。对自动发现而言, “已注册即被发现”是唯一的真相来源:SDK 注册的 schema/options 就是配置 UI 渲染的 schema/options,不存在另一份自动发现 schema 与之竞争。
  3. 重复注册 = 配置错误:同一 namespace 被 SDK 与(理论上)自动发现或其他插件 同时注册时,先到者胜、后到者在调用点同步收到宿主的 duplicate 错误并向上传播。 修复方式是移除冲突声明,而不是让 SDK 悄悄覆盖。
  4. 命名失败提前拦截:不合规范的 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 的职责;本契约只管“设置项的声明、注册、回收”。