大插件与小插件
框架用两个概念划分功能与实现:大插件承担一个完整功能,小插件承担该功能下的一个可替换分项。两者不是大小不同的同一种东西,而是两种不同的可变性承诺。
- 大插件是一棵以自身为根的 Cordis 子树,功能边界、骨架、契约与机制不可变。
- 小插件是大插件某个实现位里的一个条目,可以单独注册、选择、卸载。
相关页面:根插件名单(RPM)、插槽与实现位、policy 与写边、大插件总表。
两个概念
| 概念 | 定义 | 可变性 | 谁能影响它 |
|---|---|---|---|
| 大插件 | 一个功能单位;对外是以自身为根的 Cordis 子树。功能边界、骨架、契约(服务名、插槽名、实现位清单)与运作机制均不可变 | 不可变 | Mod 只能经机制插槽改变运作方式,不能改结构 |
| 子插件 / 小插件 | 大插件内部的一个分项,是实现位里的一个注册条目 | 可变 | Plugin 注册、选择、卸载 |
| 机制插槽 | 大插件对外的改写型入口;policy 为 public 或 trusted | 由 Mod 叠加 | Mod;不存在 closed 的对外机制插槽 |
| 实现位 | 大插件对外的 registry 注册位;具体 policy 由契约声明 | 由 Plugin 填充 | Plugin;不得新增或改名 |
判据:一个分项若不可替换,它就不是小插件,而是大插件机制的一部分。 反过来,凡声明为小插件的,必须可替换。
子树模型
Kernel(非功能,宿主)
└─ ScriptPlay 大插件(scriptplay@1,不可变)
├─ 机制插槽(给 Mod)
│ ├─ script:resolve-command
│ ├─ script:resolve-condition
│ ├─ script:resolve-presentation
│ └─ script:resolve-flow
├─ 通知型插槽(只广播事实)
│ └─ script:on-command / on-scene-enter / on-choice / on-advance
├─ 对外服务
│ └─ script:vm / script:flow / state:flags
└─ 实现位(给 Plugin)
├─ script:command-set@1 multi
├─ script:presentation@1 multi
├─ dialog:renderer@1 single
├─ choice:renderer@1 single
├─ history:view@1 single
└─ autoplay:policy@1 single以 src/core/scriptplay/contract.ts 为准。结构不可变的含义是:实现位的数量与名称、服务名、机制插槽名都由大插件固定;Plugin 只能往既定实现位里登记条目,Mod 只能在既定机制插槽上叠加。
为什么必须是不可变子树
- 整棵子树可被理解为一个功能单位。 依赖、生命周期与内部协作都在子树内闭合,外部无需知道内部结构。
- 纵切替换成立。 换掉一个大插件,不会留下指向其内部子插件的引用。前提是没有任何外部单元
inject内部子插件。 - 契约可以静态判定。 大插件版本化后,Mod 只需声明
targets,不启动本体也能判兼容。 - 卸载语义明确。 卸载大插件就是整棵子树卸载;卸载小插件只是该实现位条目消失,并按声明回落或置空。
L1–L11 硬规则
| 编号 | 规则 | 要点 |
|---|---|---|
| L1 | 禁止跨过内核通信。一切交互必须经 service 名或事件;唯一例外是信任存储。 | 大插件之间不互相 import、不传闭包;信任存储不属于扩展体系,见下文 |
| L2 | 大插件对外只暴露两类接口:机制插槽;对外服务与实现位。禁止任何单元 inject 大插件的内部子插件。 | 外部只看契约,不看内部 fiber;否则大插件无法整体替换 |
| L3 | 子插件之间禁止互相 inject 内部实现,只能经大插件提供的服务或实现位交互。 | 「子插件可被单独替换」的前提 |
| L4 | 器件上提规则:某器件被两个及以上大插件使用,必须上提为独立大插件。 | 避免跨大插件依赖内部,绕开 L2 |
| L5 | 换实现必须是注册加选择,不得替换正在被使用的 provider。用 registry 型实现位表达,不改变既有节点行为。确需替换在用 provider 的单元升为 Mod。 | 注册新条目不是写边;顶替生效条目是写边 |
| L6 | 大插件必须版本化其契约(如 scriptplay@1)。Mod 须声明 targets 及插槽;实现位同样须版本化。 | 没有版本就无法静态判定兼容 |
| L7 | 卸载大插件等于整棵子树卸载;卸载子插件等于该条目消失,且须声明回落到默认实现还是变为空缺。 | onUnload 只能是 fallback 或 vacant,不允许静默失效 |
| L8 | 实现位的数量与名称由大插件固定。Plugin 不得新增实现位、不得新增插槽。 | 新增槽位属于改大插件结构,只能由框架升版或 fork |
| L9 | 小插件的落点受 RPM 白名单约束。注册到 slots 未声明的实现位,一律拒绝。 | 由机制保证,不靠约定 |
| L10 | 大插件不得把自身内部(非对外)的 ctx 引用交给任何外部单元;对外只交付服务名与实现位句柄。 | 交出内部 ctx 等于交出 ctx.plugin() 的挂载权 |
| L11 | 运行期不存在绕过装载器的代码装载路径。一切可执行单元必须经装载管线;装载器不得暴露 eval、Function、动态 import 或 ctx.plugin 直通封装。 | RPM 焊死结构的闭合条件 |
L1 的唯一例外
信任存储是框架之外的独立数据库,独立于 Cordis 服务空间。它不经任何 Cordis 服务暴露,装载器通过宿主绑定直接访问,该绑定不注册进 ctx。任何扩展单元都不得读写它;若让它走内核通道,它就会变成可注入的服务,从而可被 Mod 拦截。
L11 的准确含义
L11 约束的是装载路径:框架不提供「把任意代码交进来就挂载」的接口,装载器只接受包来源,由管线解析、核验、判定后挂载。它不声称能阻止同一 realm 内的代码使用 eval 或 new Function;这类行为只能观测与留痕,不能作为已守护项申报。
决策口令
我要改的是「这个功能怎么运作」,还是「这个分项由谁实现」?
| 你的目的 | 落点 | 身份 |
|---|---|---|
| 新增一个渲染器、编解码器、指令集或后端实现 | 实现位登记一个条目 | Plugin |
| 改变既有节点的可观测行为:换算法、改派发、拦截服务 | 机制插槽 before / after / replace、intercept、isolate 域内 provide | Mod |
| 替换正在使用的 provider,而不是新增一个可选项 | 写边 | Mod |
| 新增整块功能或新增实现位 | 框架升版或 fork 后重新生成 RPM | 不是扩展单元 |
示例甲:Plugin 换立绘实现
Stage 的契约里有 stage:cast-renderer@1,multi、public。内置条目提供基于差分图的 image 渲染;第三方插件登记一个 spine 条目,按资产类别被选用。
- 既有
image条目仍然存在,已有场景的行为不变。 - 新条目只在选中它时生效;选择可以来自用户、场景声明或能力档位。
- 卸载时条目自动撤销;若没有其他可用条目,实现位按
onUnload的声明回落或变为空缺。 - 插件没有改写任何既有节点,因此是 Plugin,不是 Mod。
示例乙:Mod 改演出派发机制
ScriptPlay 的 script:resolve-presentation 是机制插槽。一个 Mod 在清单 writes.hooks 中声明该插槽与 replace 阶段,用自己的调度逻辑决定演出指令如何执行。
- 它改变的是「演出怎么被派发」,所有经过该机制插槽的既有节点都会受影响。
- hook 作为叠加层挂在 Mod 的作用域上,卸载即摘除,原机制恢复。
- 它需要绑定
scriptplay的契约版本;targets不匹配即拒绝装载。 - 因为产生了写边,它必须按 Mod 装载;若 policy 是
trusted,还需满足提权与同意要求。
对比之下,如果只是想新增一种演出指令执行者,应往 script:presentation@1 登记条目,那是 Plugin 行为。
分项与机制的边界
| 问题 | 小插件(实现位) | 机制的一部分 |
|---|---|---|
| 能否换一个实现而不影响其他分项? | 能 | 不能,换了就是改运作方式 |
| 是否存在固定的注册名与条目形状? | 有 | 通常没有,属于大插件内部逻辑 |
| 卸载后是否必须声明回落或空缺? | 是 | 不适用,随整棵子树卸载 |
| 典型例子 | dialog:renderer@1、asset:decoder@1、audio:output@1 | 指令解析、流程推进、条件求值、资源来源顺序 |
常见误解
| 误解 | 实际 |
|---|---|
| 大插件不可变,所以行为完全不能改 | 结构不可变;Mod 可以经机制插槽改变运作方式 |
子插件是独立根插件,可以被别的大插件 inject | 子插件在所属子树内;外部只能经服务或实现位交互 |
| 插件登记一个同名实现顶替旧实现也算 Plugin | 顶替生效 provider 是写边,应作为 Mod 装载 |
policy 是 public 就没有审查 | 仍要过签名、分类、落点、契约、能力与预算判定 |
| 第三方可以自己加一个大插件 | 不能;RPM 由构建脚本从源码生成,第三方只能 fork 或等框架升版 |
| 通过 guard 就说明代码安全 | guard 只做装载期判定,不构成技术隔离,也不评估代码是否作恶 |