Strapi @strapi/openapi 扩展指南:路由 Provider、匹配规则、装配器与处理器的贡献路径
本文以 Strapi 仓库中 @strapi/openapi 包的官方贡献指南 Overview 为主体,完整覆盖该包的全部 6 个扩展点——路由 Provider、匹配规则、装配器、上下文工厂、前后处理器与测试方法,并结合 packages/core/openapi 下的源码实现,说明每个扩展点在生成管线中的确切位置、注册方式与验证手段。读完后,你可以独立完成"让 OpenAPI 文档收录新来源的路由、过滤特定路由、为文档新增字段、新增装配层级"这一整套贡献工作。
一、定位:从生成管线看扩展点
官方 Overview 文档给出的核心信息是:@strapi/openapi 的扩展入口有且仅有 6 个,并按"改动频率"给出了优先级判断:
大多数改动集中在装配器(尤其是 operation 级的叶子装配器)或路由收集。上下文工厂和处理器用到的频率低得多。(原文:Most changes touch assemblers … or route collection. Context factories and processors are needed less often.)
原文档中的扩展点总览表如下(链接已转换为仓库内全局路径):
| 指南 | 适用场景 |
|---|---|
| Routes provider | 从一个新的 Strapi 来源收集路由 |
| Routes matcher rule | 过滤哪些路由会被写入文档 |
| Assemblers | 构建或扩展 OpenAPI 文档的各个部分 |
| Context factory | 添加带有自己上下文的新装配层级 |
| Processors | 在装配前后运行逻辑 |
| Testing | 编写或调试单元测试 |
这 6 个扩展点并非孤立存在,它们都挂载在同一条生成管线上。从入口函数 generate 的实现可以看到真实编排:
export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => {
const { type = 'content-api' } = options ?? {};
const config = {
preProcessors: new PreProcessorFactory().createAll(),
assemblers: new DocumentAssemblerFactory().createAll(),
postProcessors: new PostProcessorsFactory().createAll(),
};
// Data sources for the Strapi routes
const routeCollector = new RouteCollector(
[
new AdminRoutesProvider(strapi),
new ApiRoutesProvider(strapi),
new PluginRoutesProvider(strapi),
],
new RouteMatcher([
// Only match content-api routes
rules.isOfType(type),
])
);
const contextFactory = new DocumentContextFactory();
const generator = new OpenAPIGenerator(config, strapi, routeCollector, contextFactory);
return generator.generate();
};
由此可以确认整条管线的执行顺序(与 Processing 指南 中"Pre-processors → Assemblers → Post-processors"的三段式描述一致),并对应到 OpenAPIGenerator.generate() 中的链式调用:
_initContext—— 调用RouteCollector.collect()收集路由,再由DocumentContextFactory创建文档级上下文;_bootstrap—— 重置计时器并开始计时;_preProcess—— 依次运行所有注册的 pre-processor;_assemble—— 依次运行所有 document 级装配器(其内部再递归到 path → path-item → operation 各级子装配器);_postProcess—— 依次运行所有注册的 post-processor;_finalize—— 停止计时,将context.output.data作为document、计时结果作为durationMs返回。
也就是说:路由 Provider 与匹配规则决定了"哪些路由进入管线",装配器决定"文档每个部分怎么写",上下文工厂决定"新层级怎么携带数据",处理器决定"整份文档在装配前后还要做什么全局处理"。下面逐一展开各扩展点的官方操作步骤与源码依据。
二、扩展点 1:Routes Provider(收集新来源的路由)
官方指南 Routes Provider 描述了完整的 4 步流程。以新增一个名为 xxx 的 Provider 为例:
第 1 步:在 src/routes/providers 下新增 xxx.ts
import type { Core } from '@strapi/types';
import { createDebugger } from '../../utils';
import { AbstractRoutesProvider } from './abstract';
const debug = createDebugger('routes:provider:xxx');
export class XXXRoutesProvider extends AbstractRoutesProvider {
public get routes(): Core.Route[] {
const routes = [];
// ^ 路由收集逻辑写在这里
// 通过 this._strapi 与 Strapi 应用交互
debug('found %o routes in xxx', routes.length);
return routes;
}
}
抽象基类 AbstractRoutesProvider 的要求与文档完全吻合:构造时注入 Core.Strapi 实例并保存为 protected readonly _strapi,子类必须实现 get routes(): Core.Route[];基类还额外提供了一个 [Symbol.iterator],使 Provider 本身可直接用 for...of 逐条遍历路由。
第 2 步:从 src/routes/providers/index.ts 导出
// ...
export { XXXRoutesProvider } from './xxx';
// ^ 从这里导出
当前的 providers/index.ts 实际导出了 4 个成员:AbstractRoutesProvider、AdminRoutesProvider、ApiRoutesProvider、PluginRoutesProvider 以及类型 RoutesProvider——新 Provider 只需按同一模式追加一行。
第 3 步:从 src/routes/index.ts 重新导出
export {
// ... 其他 providers
XXXRoutesProvider,
// ^ 从这里重新导出
} from './providers';
第 4 步:在 src/exports.ts 的 generate 函数中接入
import {
// ...
XXXRoutesProvider,
// ^ 导入新创建的 provider
} from './routes';
export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => {
// ...
const routeCollector = new RouteCollector(
[
// ... 其他 providers
new XXXRoutesProvider(strapi),
// ^ 在这里实例化新 provider
]
// ...
);
// ...
};
对照 exports.ts 现状,RouteCollector 目前固定接收三个内置 Provider(Admin / API / Plugin),这正是新 Provider 的接入位置。架构文档 Architecture 中还特别指出:Route Collection 域对 OpenAPI 本身没有直接依赖,未来可能迁移到 Strapi 核心或 utils 包——这也解释了为什么"新增路由来源"是相对独立、影响面最小的扩展。
三、扩展点 2:Routes Matcher Rule(过滤被收录的路由)
官方指南 Routes Matcher Rule 以"只保留特定 HTTP 方法的 route"为例,给出 3 步流程:
第 1 步:在 src/routes/rules 下新增 is-method-in.ts
import type { MatcherRule } from '../types';
export const isMethodIn = (methods: string[]): MatcherRule => {
return (route) => methods.includes(route.method);
};
规则(Rule)的本质是一个 (route) => boolean 的判定函数。仓库中现存的参考实现是 isOfType:
export const isOfType = (type: string): MatcherRule => {
return (route) => route.info.type === type;
};
第 2 步:从 src/routes/rules/index.ts 导出
// ... 其他导出
export { isMethodIn } from './is-method-in';
// ^ 从这里导出
第 3 步:在 generate 中创建的 RouteMatcher 实例里传入新规则
export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => {
// ...
const routeCollector = new RouteCollector(
[
/* ... */
],
new RouteMatcher([
// ... 其他规则
rules.isMethodIn(['POST', 'PUT']),
// ^ 把新规则传给 matcher 实例
])
);
// ...
};
一个容易忽略的语义细节:RouteMatcher.match() 的实现是 this._rules.every((rule) => rule(route))——即所有规则必须同时满足(AND 逻辑),任一规则不通过即被排除,并且短路求值。因此新增的规则只会进一步收窄被文档化的路由集合,不能用来"放行"已被其他规则排除的路由。当前 generate 默认只挂载 rules.isOfType(type)(type 默认 'content-api'),所以若想让 isMethodIn 生效,需确认它与类型规则的组合关系符合预期。
四、扩展点 3:Assemblers(构建文档的各个部分)
这是官方 Overview 明确指出"大多数改动集中于此"的扩展点。指南 Assemblers 将装配器分为 4 个层级,每个层级有对应的接口、代表实现与工厂:
| 层级 | 接口 | 示例 | 工厂 |
|---|---|---|---|
| Document | Assembler.Document |
DocumentInfoAssembler |
DocumentAssemblerFactory |
| Path | Assembler.Path |
PathItemAssembler |
PathAssemblerFactory |
| Path item | Assembler.PathItem |
OperationAssembler |
PathItemAssemblerFactory |
| Operation | Assembler.Operation |
OperationParametersAssembler |
OperationAssemblerFactory |
从源码结构可以印证这一分层:packages/core/openapi/src/assemblers/document/ 下嵌套着 path/ → path-item/ → operation/ 目录,operation 层包含 operation-id.ts、parameters.ts、responses.ts、tags.ts、body.ts 等叶子装配器,与 OperationAssemblerFactory.createAll() 返回的 5 个装配器一一对应(OperationIDAssembler、OperationParametersAssembler、OperationResponsesAssembler、OperationTagsAssembler、BodyAssembler)。document 层则由 DocumentAssemblerFactory 产出 Metadata、Info、Server、Security、Paths 五个装配器,其中 DocumentPathsAssembler 就是一个向下注入子装配器与 PathContextFactory 的复合装配器。
4.1 添加叶子装配器(日常最常用的操作)
指南以新增 OperationSummaryAssembler(把 summary 写入 operation)为例。官方原文提示:日常改动大多是 operation 级叶子装配器(parameters、body、responses 等);只有在需要编排新的嵌套结构时才用复合装配器。
第 1 步:在 src/assemblers/document/path/path-item/operation 下创建 summary.ts
import type { Core } from '@strapi/types';
import type { OperationContext } from '../../../../../types';
import { createDebugger } from '../../../../../utils';
import type { Assembler } from '../../../..';
const debug = createDebugger('assembler:summary');
export class OperationSummaryAssembler implements Assembler.Operation {
assemble(context: OperationContext, route: Core.Route): void {
const summary = route.info.apiName ?? route.handler;
debug('assembling summary for %o %o: %o', route.method, route.path, summary);
context.output.data.summary = summary;
}
}
两条关键约定(来自原文档):
- 叶子装配器接收本层级的 context,外加接口定义的额外参数——operation 级装配器额外接收
route; - 结果一律写入
context.output.data。该对象会被调用你的那个复合装配器合并进父级输出。
第 2 步:从 operation/index.ts 导出
export { OperationSummaryAssembler } from './summary';
第 3 步:注册进 OperationAssemblerFactory.createAll()
import { OperationSummaryAssembler } from './summary';
export class OperationAssemblerFactory {
createAll(): Assembler.Operation[] {
return [
// ... 现有装配器
new OperationSummaryAssembler(),
];
}
}
若添加的是 document 级叶子装配器(例如新增一个 OpenAPI 顶层字段),同样模式应用在 src/assemblers/document/ 下,并注册进 DocumentAssemblerFactory。
4.2 添加复合装配器
复合装配器的职责是:创建子上下文 → 运行子装配器 → 把结果合并回父级输出。官方指定的参考实现是 OperationAssembler(src/assemblers/document/path/path-item/operation/operation.ts),指南给出了完整示例:
import type { Core } from '@strapi/types';
import { OperationContextFactory } from '../../../../../context';
import type { PathItemContext } from '../../../../../types';
import type { Assembler } from '../../../..';
export class OperationAssembler implements Assembler.PathItem {
constructor(
private readonly _assemblers: Assembler.Operation[],
private readonly _contextFactory: OperationContextFactory = new OperationContextFactory()
) {}
assemble(context: PathItemContext, path: string, routes: Core.Route[]): void {
const { output, ...sharedProps } = context;
for (const route of routes) {
const operationContext = this._contextFactory.create(sharedProps);
for (const assembler of this._assemblers) {
assembler.assemble(operationContext, route);
}
Object.assign(output.data, { [route.method.toLowerCase()]: operationContext.output.data });
}
}
}
接线方式:创建或扩展一个工厂(例如 PathItemAssemblerFactory),由它把子装配器与 context factory 一并注入你的复合装配器,再把这个工厂注册到上一层级(PathAssemblerFactory 或 DocumentAssemblerFactory)。仓库中现成的范本是 DocumentAssemblerFactory._createPathsAssembler()——它默认创建 PathAssemblerFactory 和 PathContextFactory(依赖注入 + 工厂模式的典型写法),将子装配器数组传给 DocumentPathsAssembler 构造函数。
官方 Tip 强调:创建子上下文时应复用父上下文的 timer 与 registries(通过 PartialContext 传入),保证计时与共享状态在整棵装配树中一致。
五、扩展点 4:Context Factory(新增装配层级)
指南 Context Factory 先划定了使用边界:
- 何时新增工厂:引入一个拥有自己输出结构(output shape)的新装配层级时;
- 何时不用新增:只在现有层级添加叶子装配器时,直接复用该层级的工厂(例如
OperationContextFactory)。
新增流程共 4 步,以虚构的 Widget 层级为例:
第 1 步:在 src/types.ts 定义 context 数据类型
import type { Context } from './context';
export type WidgetContextData = Partial<{ widgets: Record<string, unknown> }>;
export type WidgetContext = Context<WidgetContextData>;
第 2 步:创建 src/context/factories/widget.ts
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, {});
}
}
第 3 步:从 src/context/factories/index.ts 导出
export { WidgetContextFactory } from './widget';
第 4 步:在复合装配器中使用
把父上下文的共享属性传入子上下文工厂:
const childContext = this._contextFactory.create({
strapi: context.strapi,
routes: context.routes,
timer: context.timer,
registries: context.registries,
});
从 AbstractContextFactory.create() 的实现可以精确确认原文档所说的"工厂构建的 context 包含什么":
strapi与routes为必填(直接从入参解构);timer与registries优先复用父级传入的实例(context.timer ?? this._timerFactory.create()),否则新建——这正是"计时与共享状态跨层级一致"的实现基础;output.data初始化为super.create()第二个参数defaultValue,同时output.stats.time被置为零值占位。
Registries 的现状
官方文档明确说明:RegistriesFactory.createAll() 当前返回空对象,ContextRegistries 是空接口;registries 是为未来跨装配器共享状态(去重 schema、跨装配器缓存等)预留的扩展位,目前不需要任何额外配置。唯一已在用的"类注册表"机制是处理器文档提到的 context.registries.extractedComponentSchemas(见下节)。
六、扩展点 5:Pre/Post Processors(装配前后的全局逻辑)
指南 Processors 的核心事实:
- 两种处理器都接收完整的
DocumentContext; - 实例注册在
PreProcessorFactory/PostProcessorsFactory中,这两个工厂由 src/exports.ts 在generate内实例化并传入OpenAPIGenerator的config; - 执行顺序由生成器固定为:pre-processors → assemblers → post-processors(对应 generator.ts 的链式调用)。
以添加 post-processor 为例。指南先交代了现有 post-processor 的语义:ComponentsWriter 在装配完成后,把 strapi.contentAPISchemaRegistry 写入 components.schemas;路由转换期间采集到的嵌套 Zod .meta({ id }) schema 通过 context.registries.extractedComponentSchemas 合并进来,以保证 $ref 可以解析——这也印证了 registries 是跨装配阶段传递采集状态的通道。
第 1 步:创建处理器类
import type { DocumentContext } from '../types';
import type { PostProcessor } from './types';
export class ExamplePostProcessor implements PostProcessor {
postProcess(context: DocumentContext): void {
// 直接修改 context.output.data
}
}
第 2 步:注册进 PostProcessorsFactory
import { ComponentsWriter } from './component-writer';
import { ExamplePostProcessor } from './example';
export class PostProcessorsFactory {
createAll(): PostProcessor[] {
return [new ComponentsWriter(), new ExamplePostProcessor()];
}
}
Pre-processor 则实现带 preProcess(context: DocumentContext) 方法的 PreProcessor 接口,以同样方式注册进 PreProcessorFactory。
官方给出的选型原则值得原样遵循:能用装配器解决的文档分区构建,优先用装配器;处理器只用于必须在整轮装配之前/之后执行的横切逻辑。
七、扩展点 6:测试
指南 Testing 规定:单元测试位于 packages/core/openapi/__tests__/,从包目录运行:
cd packages/core/openapi && yarn test:unit
共享辅助代码在 __tests__/fixtures/ 与 __tests__/mocks/。仓库中对应的真实测试文件包括 document-assemblers.test.ts、operation-assemblers.test.ts、routes 目录以及 zod-to-openapi.test.ts 等。
官方文档给出三类被测对象的写法:
7.1 测试路由 Provider
只 mock 你的 Provider 实际读取的 Strapi 属性;__tests__/mocks/strapi.mock.ts 中的 StrapiMock 覆盖了常见的 apis 与 plugins 结构:
import type { Core } from '@strapi/types';
import { ApiRoutesProvider } from '../../src/routes';
import { StrapiMock } from '../mocks';
it('returns registered API routes', () => {
const strapi = new StrapiMock() as unknown as Core.Strapi;
const provider = new ApiRoutesProvider(strapi);
expect(provider.routes.length).toBeGreaterThan(0);
});
7.2 测试匹配规则
构造最小 Core.Route 直接喂给 RouteMatcher:
import type { Core } from '@strapi/types';
import { RouteMatcher } from '../../src/routes';
const route: Core.Route = {
method: 'GET',
path: '/api/articles',
handler: '',
info: { type: 'content-api' },
};
expect(new RouteMatcher([(r) => r.method === 'GET']).match(route)).toBe(true);
7.3 测试装配器
用对应层级的工厂创建 context,运行装配器,断言 context.output.data:
import type { Core } from '@strapi/types';
import * as z from 'zod/v4';
import { OperationParametersAssembler } from '../../src/assemblers/document/path/path-item/operation';
import { OperationContextFactory } from '../../src/context';
const context = new OperationContextFactory().create({ strapi: {} as Core.Strapi, routes: [] }, {});
new OperationParametersAssembler().assemble(context, {
method: 'GET',
path: '/api/articles/:id',
handler: '',
info: { type: 'content-api' },
request: {
params: { id: z.string() },
query: { locale: z.string().optional() },
},
});
expect(context.output.data.parameters).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'id', in: 'path' }),
expect.objectContaining({ name: 'locale', in: 'query' }),
])
);
指南特别指出:route 上的 request 与 response Zod schema 是 operation 级装配器的主要输入;__tests__/operation-assemblers.test.ts 中有镜像 content API addQueryParams / addInputParams 行为的完整用例可参照。
7.4 调试失败用例
在包目录启用该包的 debug 输出:
cd packages/core/openapi && DEBUG=strapi:core:openapi:* yarn test:unit
这与源码中各模块使用的命名空间(如 generator、routes:provider:xxx、assembler:summary)相互印证。
八、选型决策与扩展点速查
结合 Overview 的优先级提示与各指南,可以把"该改哪里"压缩为一张决策表:
| 你想做的事 | 扩展点 | 主要改动文件 |
|---|---|---|
| 让文档收录新的路由来源(如新的 Strapi 子系统) | Routes Provider | src/routes/providers/*、src/routes/providers/index.ts、src/routes/index.ts、src/exports.ts |
| 排除/收窄被文档化的路由 | Matcher Rule | src/routes/rules/*、src/routes/rules/index.ts、src/exports.ts |
| 为文档新增字段或 section(operation 级最常见) | 叶子装配器 | src/assemblers/document/** 对应层级目录 + 对应 Factory.createAll() |
| 引入新的嵌套文档结构 | 复合装配器(+ 新 Context Factory) | src/assemblers/**、src/context/factories/*、src/types.ts |
| 整份文档装配前/后的全局处理 | Processor | src/pre-processor/* 或 src/post-processor/* 的工厂 |
| 验证以上任何改动 | 单元测试 | packages/core/openapi/__tests__/ |
两个跨扩展点的重要约定值得最后强调:
- 一切改动都在工厂的
createAll()中登记,最终由generate()消费。 generate 集中实例化了PreProcessorFactory、DocumentAssemblerFactory、PostProcessorsFactory、RouteCollector(含三个内置 Provider)与DocumentContextFactory,因此新组件若没有出现在相应工厂的返回列表中,管线不会感知到它。 - 包根导出与公开 API 解耦。 架构文档说明该包在 src/exports.ts 中集中管理公开 API 的全部导出,包根导出与面向编程调用的公共 API 相互独立——这也意味着对
generate的签名或行为调整属于@experimental范畴(Usage 文档 明确声明 OpenAPI 生成功能是实验性的,其行为与输出可能不遵循 semver 变化),实验性 CLI 为strapi openapi generate [-o, --output <path>]。
以上各扩展点均位于 @strapi/openapi 包内,源码根目录为 packages/core/openapi/src/,测试位于 packages/core/openapi/__tests__/。按"装配器/路由收集优先,处理器与上下文工厂次之"的顺序理解这 6 个扩展点,即可覆盖对 Strapi OpenAPI 生成管线的绝大多数定制需求。
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