Strapi @strapi/openapi 扩展实战:为新装配层级添加 Context Factory
本文基于 Strapi 官方贡献指南 Context Factory 编写,讲解当 @strapi/openapi 包的 OpenAPI 文档生成流水线需要引入一个新的装配(assembly)层级时,如何按官方模式定义上下文数据类型、创建 Context Factory 并将其接入组合装配器。读完后你将能够理解 Context / AbstractContextFactory 在生成流水线中的职责,独立走完「定义类型 → 建工厂 → 导出 → 在组合装配器中使用」的完整四步流程,并掌握 timer 与 registries 在父子上下文之间共享的机制。
背景:Context Factory 在生成流水线中的位置
@strapi/openapi 采用「路由收集 → 上下文初始化 → 装配 → 后处理」的流水线生成 OpenAPI 3.1 文档。OpenAPIGenerator 的 generate() 方法按固定顺序执行:收集路由并用 DocumentContextFactory 创建顶层 DocumentContext、启动计时器(_bootstrap)、依次运行 pre-processors、逐个运行 document 级 assemblers、运行 post-processors,最后由 _finalize() 停止计时器并把耗时写入 output.stats.time。
装配器(assembler)按 OpenAPI 文档结构组织成一棵树,从源码 assemblers/types.ts 可以确认四个现有层级及其上下文类型:
| 装配层级 | 输出数据形状 | 上下文类型 | 对应 Factory |
|---|---|---|---|
| Document | Partial<OpenAPIV3_1.Document> |
DocumentContext |
DocumentContextFactory |
| Path | Partial<OpenAPIV3_1.PathsObject> |
PathContext |
PathContextFactory |
| PathItem | Partial<OpenAPIV3_1.PathItemObject> |
PathItemContext |
PathItemContextFactory |
| Operation | Partial<OpenAPIV3_1.OperationObject> |
OperationContext |
OperationContextFactory |
这四个上下文类型集中定义在 src/types.ts,四个工厂类则位于 src/context/factories/。每个组合装配器(如 OperationAssembler)在向下装配时,都会用自己层级的 Context Factory 为下一级创建独立的 typed context——这正是本文要讲解的扩展点。整体流水线可参考 Architecture 文档,各扩展点的分工总览见 Contributing Overview。
什么时候需要新增一个 Context Factory
官方文档给出的判断准则是:
- 引入一个带有独立输出形状(output shape)的新装配层级时,才需要新增对应的工厂类。每个 assembler 层级都运行在由匹配工厂创建的 typed context 上(
DocumentContext、OperationContext等)。 - 只是在某个现有层级上添加叶子装配器(leaf assembler)时,直接复用现有工厂即可,例如操作级装配器统一复用
OperationContextFactory。
这个准则与源码结构一致:现有四层的工厂类(以 OperationContextFactory 为例)都只泛型绑定了各自的 OpenAPI 数据片段,装配器与工厂一一对应。只有当你想在 Document 与 Path 之间(或树的其他位置)插入一个产出全新 OpenAPI 结构片段的层级时,才需要照此模式新增一套「类型 + 工厂」。
Context 类型体系:各字段的作用
新增工厂前必须先理解 src/context/types.ts 中的三个核心类型:
export interface ContextOutput<T> {
data: T; // 本层级装配产出的 OpenAPI 数据片段
stats: Stats; // 耗时统计,由 Timer 填充
}
export interface Context<T = unknown> {
routes: Core.Route[]; // 待文档化的全部路由(必填)
strapi: Core.Strapi; // Strapi 实例(必填)
timer: Timer; // 计时器,可来自父级
registries: ContextRegistries; // 共享注册表,可来自父级
output: ContextOutput<T>;
}
export type PartialContext<T> = Partial<Pick<Context<T>, 'timer' | 'registries'>> &
Required<Pick<Context<T>, 'strapi' | 'routes'>>;
export interface ContextFactory<T> {
create(context: PartialContext<T>, defaultValue: T): Context<T>;
}
要点:
PartialContext<T>通过Required/Partial精确约束了工厂入参:strapi和routes是必填的,只有timer和registries两个字段允许缺省——缺省正是「从父级共享或新建」的开关。Context<T>的泛型参数T决定该层级output.data的形状,这也是官方文档强调「新装配层级需要自己的 output shape」的类型学依据。Stats/TimeStats(L8-L16)持有startTime、endTime、elapsedTime,由 Timer 提供:start()在已启动时抛错、stop()在未启动时抛错、reset()清零,状态机式的约束保证了每个 context 的计时语义清晰。ContextRegistries定义为ReturnType<RegistriesFactory['createAll']>,详见下文 Registries 一节。
AbstractContextFactory:所有工厂的公共基类
新增工厂时你并不直接实现 ContextFactory<T> 接口,而是继承 AbstractContextFactory。官方文档对 AbstractContextFactory.create() 行为的描述,在源码中可以逐条印证:
public create(context: PartialContext<T>, defaultValue: T): Context<T> {
const { strapi, routes } = context;
// Allow overrides to share registries and timer in case the context is used in sub-assemblers
const timer = context.timer ?? this._timerFactory.create();
const registries = context.registries ?? this._registriesFactory.createAll();
// Default output initialized with the given default value
const output = this.createDefaultOutput(defaultValue);
return { strapi, routes, timer, registries, output };
}
strapi和routes必填,直接从入参取用;timer和registries遵循「父级提供则复用、否则新建」的短路逻辑(L20-L21),注释也明确说明这是为了子装配器(sub-assemblers)共享同一 timer 与 registries;output.data初始化为传给super.create()的defaultValue,由createDefaultOutput()同时生成全零的stats(L29-L34)。
两个依赖——RegistriesFactory 与 TimerFactory——通过 protected 构造器注入,因此子类工厂只需在构造器中 super(registriesFactory, timerFactory),并保持默认参数(new RegistriesFactory() / new TimerFactory())以便装配器可以零参实例化。
四步走:添加一个新的 Context Factory
以下四步完整继承自官方指南,并对照仓库中的真实实现加以说明。假设你需要一个名为 Widget 的新装配层级。
第 1 步:在 src/types.ts 中定义上下文数据类型
在 src/types.ts 中按现有模式追加数据片段类型与 context 别名:
import type { Context } from './context';
export type WidgetContextData = Partial<{ widgets: Record<string, unknown> }>;
export type WidgetContext = Context<WidgetContextData>;
对照现有实现,这里的模式完全一致:DocumentContextData = Partial<OpenAPIV3_1.Document>、PathItemContextData = Partial<OpenAPIV3_1.PathItemObject> 等(L4-L14)。数据片段通常取 openapi-types 中对应结构对象的 Partial 形态,context 别名则通过 Context<T> 绑定该片段。
第 2 步:创建 src/context/factories/widget.ts
照 OperationContextFactory 的结构编写:
import { RegistriesFactory } from '../../registries';
import type { WidgetContext, WidgetContextData } from '../../types';
import { TimerFactory } from '../../utils';
import type { PartialContext } from '../types';
import { AbstractContextFactory } from './abstract';
export class WidgetContextFactory extends AbstractContextFactory<WidgetContextData> {
constructor(
registriesFactory: RegistriesFactory = new RegistriesFactory(),
timerFactory: TimerFactory = new TimerFactory()
) {
super(registriesFactory, timerFactory);
}
create(context: PartialContext<WidgetContextData>): WidgetContext {
return super.create(context, {});
}
}
两个关键细节:
- 类泛型是 数据片段类型
WidgetContextData(而非WidgetContext),基类用它推导output.data的形状;create()的返回值类型再收窄为完整的WidgetContext,与 DocumentContextFactory 的写法相同(路径对应 src/context/factories/document.ts)。 super.create(context, {})中的{}是defaultValue:output.data从空对象开始累积,各叶子装配器随后向其中填充字段。OpenAPI 文档片段都适合以{}为初始值。
第 3 步:从 src/context/factories/index.ts 导出
当前 factories/index.ts 导出了五个成员(AbstractContextFactory 加四个层级工厂):
export { AbstractContextFactory } from './abstract';
export { DocumentContextFactory } from './document';
export { OperationContextFactory } from './operation';
export { PathContextFactory } from './path';
export { PathItemContextFactory } from './path-item';
在其后追加一行即可:
export { WidgetContextFactory } from './widget';
第 4 步:在组合装配器中使用
组合装配器构造时接收自己的 Context Factory(惯例上作为带默认值的构造参数,如 OperationAssembler 的 contextFactory: OperationContextFactory = new OperationContextFactory()),在 assemble() 中为子层级创建 context 时,把父上下文的共享 props 透传下去,使整棵装配树共享同一个 timer 与 registries。官方文档给出的显式写法:
const childContext = this._contextFactory.create({
strapi: context.strapi,
routes: context.routes,
timer: context.timer,
registries: context.registries,
});
仓库中存在两种等价的实际写法,可作为参考:
- 显式构造 initProps(PathItemAssembler):
_createPathItemContext()里逐项把strapi、registries、routes、timer从父PathContext拷入PartialContext<PathItemContextData>,再调用create(initProps); - 解构透传(OperationAssembler):
const { output, ...defaultContextProps } = context;剔除掉本层级的output后,直接把剩余字段传给this._contextFactory.create(defaultContextProps)。
注意两种写法都刻意排除了父级的 output——子 context 的 output.data 必须从自己的 defaultValue 重新开始,装配完成后组合装配器再把 childContext.output.data 合并回父级输出(如 operation.ts L46-L52 中 Object.assign(output.data, { [methodIndex]: operationObject }))。各层级的注入链(DocumentAssemblerFactory → PathAssemblerFactory → PathItemAssemblerFactory → OperationAssemblerFactory)展示了同一模式如何逐层传递,例如 PathItemAssemblerFactory._createOperationAssembler。
Registries:预留的共享装配状态
官方文档对 registries 的说明是:RegistriesFactory.createAll() 目前返回空对象,ContextRegistries 是空接口,registries 是为「未来的共享装配状态」(如去重后的 schema、跨装配器缓存)预留的扩展位,当前无需任何额外配置。
对照当前源码,情况与该描述基本一致,但已出现具体字段:RegistriesFactory.createAll() 返回 { extractedComponentSchemas: {} },即一个预留用于存放已抽取组件 schema 的空记录;相应地,ContextRegistries 是通过 ReturnType<RegistriesFactory['createAll']> 推导出的结构类型,而非字面空接口。从源码结构看,「跨装配器共享去重 schema」这一官方设想已经在 registries 的数据结构中落地了入口,只是当前装配流程尚未向其写入内容。实际结论不变:新增 Context Factory 时无需为 registries 做任何特殊配置,AbstractContextFactory 会自动为顶层 context 创建一份,并沿装配树向下共享。
小结与延伸阅读
- 新增 Context Factory 只发生在「引入新装配层级」时;叶子装配器一律复用现有工厂;
- 四步流程为:在 src/types.ts 定义
*ContextData与*Context别名 → 在 src/context/factories/ 下继承AbstractContextFactory建工厂并默认初始化 output 为{}→ 更新 barrel 导出 → 在组合装配器中透传strapi/routes/timer/registries创建子 context; - 工厂的复用语义由
PartialContext<T>的类型约束保证(必填两字段、可共享两字段),计时与共享状态分别由Timer和Registries承载。
相关路径:
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00