插件开发范式

声明和更新插件配置

用一个 Valibot object schema 统一配置类型、默认值、归一化和校验。

配置适合会随部署或用户选择变化的值,例如服务地址、超时和开关。 在 Plugin 中声明一个 Valibot schema,Pluxel 会从中得到类型、默认值、校验规则和 Workbench 配置表单。

开始前,你应已有一个能启动的 Plugin。先给配置加默认值,在 init() 中读取,再从工作台修改它。 普通配置不保存密钥;密码、Token 等使用 Vault。

本次任务阅读位置
新增或调整字段最小 schema → 声明规则
保存后立即生效在线更新,检查保存和应用两个结果
拆分局部配置Part config,字段名会成为公开配置路径
部署提供固定输入env/file 绑定,与管理保存值分层
调整编辑界面配置表单,继续复用同一 schema

先添加一个有默认值的字段

import { ,  } from '@pluxel/core'
import * as  from 'valibot'

@({ : 'Worker' })
export class  extends  {
	private readonly  = this..(
		.({
			: .(.(.(), .(), .(1)), 4),
		}),
	)

	protected override () {
		this...('worker configured', { : this.. })
	}
}

首次没有保存配置时,启动日志应显示 concurrency: 4;工作台的标准配置页应出现此字段。 输入 0 会校验失败。保存为 8 后,若没有注册下文的在线更新处理,重启插件才会应用新值。 “保存成功”与“运行中已应用”需要分别检查。

添加标题、约束和更多字段

// @filename: config.ts
import * as  from 'valibot-form'
import * as  from 'valibot'

export const  = .({
	: .(.(.(), .({ : '启用同步' })), true),
	: .(.(), .(), .({ : '上游地址' })),
	: .(
		.(
			.(),
			.(),
			.(1),
			.(32),
			.({ : '并发数' }),
			.({ : 1 }),
		),
		4,
	),
})
// @filename: WorkerPlugin.ts
import { ,  } from '@pluxel/core'
import {  } from './config.ts'

@({ : 'Worker' })
export class  extends  {
	private readonly  = this..()

	protected override () {
		this...('worker configured', {
			: this..,
			: this..,
		})
	}
}

在测试中验证配置

日常手动配置使用 Workbench;以下 host 指 插件测试宿主,适合检查初始值和更新结果。 正在运行的开发应用应通过 开发控制台修改配置。 测试中,首次启动可以传入 initialConfig:

const worker = await host.start(WorkerPlugin, {
	initialConfig: {
		endpoint: 'https://api.example.com',
		concurrency: 8,
	},
})

node 已拥有 committed config 或进入过 lifecycle 后,使用运行期配置 API:

const result = await host.config.patch(WorkerPlugin, {
	endpoint: 'https://api.example.com',
	concurrency: 12,
})

// 已注册 configs.onUpdate() 且处理成功时,application 为 'applied'。
// 否则检查 'saved-not-applied',再按业务需要显式重启。
expect(result.ok).toBe(true)

initialConfig 不会根据当前状态偷偷变成 live update;越过 bootstrap boundary 后继续传它会明确失败。Native 与 Vite 应用的持久化和 reload 行为由宿主决定;Plugin 只读取校验后的配置。配置保存与显式 restart 是两个独立操作,host.config.patch() 不会隐式重启; 运行中的原地更新只通过 configs.onUpdate() 通知。

声明规则

工具链依赖这个稳定形状提取 metadata,因此:

  • configs.use() 必须是 concrete @Plugin 或 direct PluginPart subclass 的普通 class field;
  • 不能放进 constructor、method、嵌套 class 或 helper function;
  • 不能使用 JavaScript #private field,因为 runtime 无法注入;
  • 每个 Plugin/Part class 各自最多一次,并且 schema 必须产出 object;
  • schema expression 要能由 semantic pass 追踪,不用动态 runtime 分支拼接。

使用 TypeScript private 或 protected 字段即可。构造器和其他字段初始化器中提前读取配置会被构建检查拒绝;读取时机见下节。 configs 只在 Plugin/Part 子类内部使用;测试和宿主通过配置 API 操作,不直接改实例字段。 configs.use(schema) 返回 schema output 的深只读 ConfigSnapshot,与运行时深冻结一致,包括嵌套对象、array 和 tuple。需要排序或编辑的临时数据先复制,例如 [...this.config.targets];不通过类型断言绕过冻结。

默认值与读取时机

默认值、trim、范围和跨字段校验统一写在 schema 中;业务代码直接使用校验结果,不再加 ?? default。 字段标题和说明用 f.formMeta();必填、格式和范围由 Valibot 表达。 配置在构造完成后、init() 前注入。依赖配置的连接在 init() 中创建,并立即登记 cleanup;constructor 和其他 field initializer 不读取配置。

让运行中的 Plugin 接收配置更新

保存配置会先持久化 desired 值,不会自动重启插件。先看 mutation 的 ok,再看 application:

application已发生什么调用方下一步
applied当前 generation 已确认应用核对业务状态
deferred已保存,节点未运行下次启动时应用
saved-not-applied已保存,当前 generation 未确认应用查看 apply report,修正处理器或显式重启

连接或服务可以原地更新时,在 init() 中注册一次 configs.onUpdate();处理完成后,框架才确认 applied:

import { ,  } from '@pluxel/core'
import * as  from 'valibot'

const  = .({
	: .(.(), 5_000),
	: .(.(), 4),
})

@()
export class  extends  {
	private readonly  = this..()
	private  = { : 0, : 0 }

	protected override () {
		this. = { ...this. }
		this..(this., ({  }) => {
			this. = {
				: .,
				: .,
			}
		})
	}

	() {
		return this.
	}
}

更新函数收到两个只读快照和一个取消信号。这里的一次运行指插件从启动到停止或重启之前的这段生命周期:

  • applied 是本次运行已确认应用的配置;
  • desired 是本次已保存、等待应用的配置;
  • signal 在插件停止、重启或热替换时取消。

listener resolve 表示 Plugin 确认自己已经处理更新。框架随后更新 config field 与 applied revision,但不会检查 Plugin 的普通字段、连接或 外部系统,也不会回滚 listener 已经产生的副作用。需要并发一致读取时,先构造完整 runtime object,再像示例一样一次替换引用。

注册只允许发生在声明该 field 的 Plugin/Part init() 中,每个 declaration、每个 generation 一次。Listener 应保持幂等,并能从当前 真实 runtime state 收敛到最新 desired。如果只能通过完整重建安全应用配置,就不要注册 listener;保存结果会是 saved-not-applied,由用户或宿主显式 restart。Listener 内不要等待另一个 config mutation、restart 或 graph operation,因为这些操作 与当前通知使用同一个 coordinator queue。

缺失值、显式值和归一化

host 提供的是 raw config record,schema 负责把它变成冻结的 normalized snapshot:

host config patch
  -> raw object
  -> Valibot parse/default/transform
  -> frozen config snapshot
  -> Plugin init

undefined/缺失是否允许、空字符串是否有效、数字是否转换,都由 schema 明确决定。不要依赖表单控件或环境变量碰巧给出正确类型。

如果校验失败,Plugin 不进入 running,错误以字段 path 形式反馈给宿主。不要在 Plugin 内捕获 ConfigValidationError 后继续启动。

normalized output 必须是无环的 plain object/array tree,leaf 使用 JSON-compatible primitive。runtime 会复制并冻结这棵树;Date、 Map、Set、class instance、function、symbol、getter/setter 和 cycle 都会被拒绝。需要领域对象时,把它保存成稳定的 plain value, 再在 Plugin init() 中显式构造资源;不要让 schema transform 把带 identity 或 behavior 的对象塞进 config snapshot。

嵌套相关设置与 Part config

每个 Plugin 声明一个 object schema;不同配置域使用嵌套 object 组织,不要多次调用 configs.use():

import * as  from 'valibot'

const  = .({
	: .({
		: .(),
		: .(.(), 5_000),
	}),
	: .({
		: .(.(), 3),
		: .(.(), 250),
	}),
})

是否拆成另一个 Plugin 的判断标准不是“字段很多”,而是这部分是否拥有独立依赖、失败、启停或治理意义。

如果设置对应一个 owner-bound PluginPart,Part 可以声明自己的 schema,配置会自然进入 occurrence field path:

const CacheConfig = v.object({ maxEntries: v.optional(v.number(), 1_000) })
const SearchConfig = v.object({ endpoint: v.string() })

class CachePart extends PluginPart<SearchPlugin> {
	private readonly config = this.configs.use(CacheConfig)
}

@Plugin()
class SearchPlugin extends BasePlugin {
	private readonly cache = this.parts.use(CachePart)
	private readonly config = this.configs.use(SearchConfig)
}

对应 raw record:

{
	"endpoint": "https://search.example.com",
	"cache": { "maxEntries": 2000 }
}

父 schema 仍占 root,Part schema 只接收 cache subtree;两边分别执行 default/transform,随后合成冻结 snapshot。父 schema output 不能使用与 direct Part field 相同的 key。Part field rename 会改变公开配置 path,应按配置 contract 变更处理。 这个 path 是静态配置坐标,不会同时作为 Part Context 上可读的 runtime identity。 Part config 属于静态 owner schema:即使 optional provider absent、对应 Part 没有产生业务 effects,这个 subtree 仍会执行 default、transform 和 validation。需要“未启用时不要求凭据”等语义时,在 schema 中使用带 enabled discriminator 的 object 明确表达,不根据 runtime catalog 动态改变配置契约。

Part 的静态声明、依赖与生命周期边界见使用 PluginPart 组织内部资源。

敏感信息

普通 Plugin config 会被宿主配置系统和 Workbench 管理面读取,不应当默认承载 secret。token、私钥和长期 credential 优先由宿主 secret provider、部署环境或 Vault 管理,再在 server-side capability 中使用。

不要把 secret 放进:

  • browser-safe Workbench DTO/API;
  • config form metadata、description 或 option label;
  • structured log properties;
  • status snapshot、command output 或序列化错误。

配置表单

valibot-form metadata 可以让同一 schema 生成字段、说明、布局和控件选择。它不改变 Valibot validation,也不建立第二份 config model。

使用 配置 Playground 可编辑 schema、操作生成的表单,并比较原始输入与 Valibot 输出。完整 metadata 与 React adapter 见 Valibot 配置表单。浏览器表单只是编辑界面;提交到宿主后仍必须由 server runtime 使用同一个 schema 校验。

Workbench 把父 schema 显示为“常规”分区,把 Part schema 按 nested path 显示为独立分区。配置操作栏固定在内容区顶部,切换分区时会保留 各自的滚动位置和未保存草稿;分区名称后的圆点与“待保存”计数用于提示尚未提交的变化。使用 Ctrl/⌘ + S 保存当前分区,或使用 Ctrl/⌘ + Shift + S 一次保存当前 Plugin 的全部已修改分区。所有分区编辑同一个 Plugin config owner;保存当前分区或全部分区都会在 server 重新验证完整 composite record。只有所有变化的 Plugin/Part declaration 都注册 listener 时才通知当前 generation;否则只保存 desired config,不会单独更新 Part 或隐式 restart。

验证

测试默认值、非法输入、归一化输出,以及保存后实际 application 状态;schema 变化后重新构建并检查标准配置表单。 用 inspect 定位 schema 与应用绑定,用开发控制台核对当前生效值。

部署绑定与来源优先级由Host 配置维护;插件只声明 schema 并消费校验后的值。

最后更新于

本页目录