Halo 公共 API 库(api/)开发指南:扩展模型、服务契约与安全抽象的兼容性治理
api/ 是 Halo 项目中独立成库的公共 API 模块,承载了整套扩展模型(Extension Model)、服务契约(Service Contracts)与安全抽象(Security Abstractions),既被后端应用 application/ 消费,也是所有第三方插件在编译期唯一依赖的 ABI。本指南围绕仓库内 api/AGENTS.md 的治理约定展开,说明该模块的职责边界、构建命令、编码规范与评审红线,并结合源码给出可落地的实践依据;读完你既能理解"改哪个包会影响谁",也能掌握如何在不破坏下游兼容的前提下向 API 库提交变更。
api/ 是什么:全项目共享的兼容性表面
从模块治理的角度看,Halo 是一个前后端同仓的全栈 Monorepo,根目录 AGENTS.md 将其划分为 API 库、后端应用、前端与两组 BOM(platform/application/、platform/plugin/)。其中 api/AGENTS.md 开宗明义地定义了 api/ 的本质:
api/is the public API library: the extension model, service contracts, and security abstractions consumed by the application and by plugins. Changes here affect downstream compatibility.
这句话包含三层信息:
- 它是"库"而非"应用":不承载可独立启动的服务逻辑,而是被应用与其他工程链接的 Java Library。
- 它是插件的唯一编译面:第三方插件基于 Halo 构建时,接触到的类型、接口全部来自这里,因此该目录的每一次变更都会波及所有已发布的插件。
- 它必须保持向后兼容:后续小节中的"偏好增量变更""变更需显式批准"等约定,均由这一特性推导而来。
工程配置层面同样印证了它的定位:api/build.gradle 中 group = 'run.halo.app'、description = 'API of halo project, connecting by other projects.',并通过 halo.publish 与 publishing { publications.named('mavenJava') } 把模块发布为独立 Maven 构件,同时启用 withSourcesJar() 与 withJavadocJar(),说明该库对外还承担了文档(Javadoc)供给的职责。也就是说,模块内的类型注释会直接成为下游插件开发者阅读的 API 文档。
代码布局:五大职责与对应的包结构
文档规定源码位于 api/src/main/java/run/halo/app/,测试位于 api/src/test/。从实际目录看,源码按领域进一步组织为以下包(下表为真实存在的顶层包与代表性类型):
| 包 | 职责 | 代表性类型(源码级佐证) |
|---|---|---|
extension |
扩展模型核心:自定义资源类型定义、客户端、索引与 Watch | Extension、ExtensionClient、ReactiveExtensionClient、GVK、Scheme、Watcher、ListResult、PageRequest(见 extension 包) |
extension.index.query |
扩展索引的查询条件 DSL | And、Or、EqualCondition、LabelCondition、Queries 等(见 index/query 包) |
extension.controller |
声明式控制器框架(Kubernetes 风格的 reconcile 循环) | Controller、ControllerBuilder、Reconciler、ExtensionWatcher、RequeueException(见 controller 包) |
core.extension |
Halo 内建的具体扩展资源类型 | Post、Category、Tag、SinglePage、Comment、Menu、Theme、User、Role、Plugin、Setting、Attachment 等(见 core/extension 包) |
security |
认证/授权与安全过滤器抽象 | AdditionalWebFilter、AuthenticationSecurityWebFilter、PersonalAccessToken、CryptoService、DeviceService(见 security 包) |
plugin |
插件 SPI 与生命周期 | BasePlugin、PluginContext、SettingFetcher、ReactiveSettingFetcher、SpringPluginManager、extensionpoint.ExtensionGetter(见 plugin 包) |
notification |
通知中心的契约 | NotificationCenter、NotificationReasonEmitter、ReactiveNotifier、ReasonPayload、UserIdentity(见 notification 包) |
search |
搜索引擎抽象与文档事件 | SearchEngine、SearchService、HaloDocumentsProvider、HaloDocument 及增删重建事件(见 search 包) |
theme |
主题侧扩展点:Finder、模板头部/页脚处理 | Finder、TemplateHeadProcessor、TemplateFooterProcessor、CommentWidget、ReactivePostContentHandler(见 theme 包) |
event / infra / content / migration |
领域事件、基础设施抽象、内容契约与迁移 | PostPublishedEvent、SystemInfoGetter、ExternalUrlSupplier、PostContentService、Backup 等 |
需要特别指出的是,api/ 中大量接口以响应式(Reactive) 形态存在,例如 ReactiveExtensionClient、ReactiveNotifier、ReactiveSettingFetcher、ReactivePostContentHandler。这与 Halo 后端 Spring Boot WebFlux + R2DBC 的技术栈一一对应——API 库并非"贫血的 DTO 仓库",而是直接承载了与运行时异步语义强绑定的契约。
常用命令:在仓库根目录执行
api/AGENTS.md 只约定两条必需命令,均需在仓库根目录通过 Gradle Wrapper 执行:
./gradlew :api:test # 运行 API 库的单元/集成测试
./gradlew spotlessApply # 格式化 Java 与 Markdown
结合工程配置可以补充以下细节:
- 测试任务:
api/build.gradle中tasks.named('test')配置了useJUnitPlatform(),即以 JUnit Platform(JUnit 5)运行;测试结束后finalizedBy jacocoTestReport会自动生成覆盖率报告,其中 XML 报告被开启(xml.required = true),供 CI 做覆盖率门禁消费。 - Java 版本:
api/build.gradle通过 Java Toolchain 声明languageVersion = JavaLanguageVersion.of(21),同时options.release = 21强制以 release 21 编译,确保运行在 Java 21 及以上环境;源码编码固定为 UTF-8。 - 格式化:
spotless { java { palantirJavaFormat("2.90.0").formatJavadoc(true) } },即使用 Palantir Java Format 2.90.0,且会顺带格式化 Javadoc——这也呼应了上文"Javadoc 即对外文档"的定位;根目录 AGENTS.md 则说明仓库级spotlessApply还会覆盖 Markdown、JSON 与 properties。 - 可选的扩展命令:若改动影响后端,可运行
./gradlew :application:test;前端改动可用pnpm -C ui typecheck && pnpm -C ui lint校验(详见 根目录 AGENTS.md 的 Quick Commands)。
编码约定:JVM 版本、格式与增量原则
文档将 api/ 的编码约定浓缩为四点,其中兼容性策略是全篇的灵魂:
1. Java 21 + 4 空格缩进 + Spotless 强制格式。 这与整个后端应用保持一致(同受根目录 AGENTS.md 约束),并依赖 platform/application/ BOM 统一依赖版本管理,避免在 api/build.gradle 中散落硬编码版本号。
2. API 是兼容性表面,偏好"加法式变更"。 允许新增类型与方法(additive changes),但任何破坏性变更——例如修改既有方法签名、删除公开类型、改变接口语义——必须在合并前显式提出并评审。推荐的动作是:
- 新增能力优先通过新增重载/新接口/新资源类型表达,而不是改动旧签名;
- 确需破坏兼容时,先在对齐的 PR/讨论中说明受影响范围(如哪些插件会编译失败)再动手。
3. 扩展定义与 Schema 的落地位置在 application 侧。 文档明确:"Extension definitions and schemas live in application/src/main/resources/extensions/; when you change them, update application/ and ui/ together." 这一点很容易被误读——api/ 里的 core.extension 是 Java 类型定义,而真正声明"系统中有哪些扩展资源、有哪些扩展点实现"的 YAML 全部收敛在 extensions 目录,例如 extension-definitions.yaml、extensionpoint-definitions.yaml 及一批 role-template-*.yaml。详见下一节。
4. 测试使用 JUnit 5,命名规范化。 单元测试命名 XxxTest,集成测试命名 XxxIntegrationTest。从 api/src/test/java/run/halo/app/ 的真实文件可以看出这套规则的实际落地,例如模型校验类 CategoryTest、PostTest、MenuTest、ThemeTest,查询与选择器类 QueriesTest、LabelSelectorTest、OperatorTest,工具类 JsonUtilsTest、PathUtilsTest、HttpSecurityUtilsTest,以及控制器框架的 ControllerBuilderTest、RequestSynchronizerTest 等。
扩展定义与 Schema 的三侧联动:api、application、ui
扩展模型虽然是 api/ 的核心职责,但 schema 的"权威 YAML"并不在本模块,而是统一收敛在 application/src/main/resources/extensions/extension-definitions.yaml。该文件的典型结构如下(以通知与搜索为例):
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
name: halo-email-notifier
spec:
className: run.halo.app.notification.EmailNotifier
extensionPointName: reactive-notifier
displayName: "邮件通知器"
description: "支持通过电子邮件向用户发送通知"
---
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
name: search-engine-lucene
spec:
className: run.halo.app.search.lucene.LuceneSearchEngine
extensionPointName: search-engine
displayName: "Lucene 搜索引擎"
description: "Halo 自带的本地搜索引擎"
从中可以提炼出 api/ 契约与 application 实现之间的映射规律:
- YAML 中
spec.className指向的类(如run.halo.app.security.authentication.login.UsernamePasswordLogoutHandler、run.halo.app.theme.dialect.TemplateGlobalHeadProcessor)大多实现或消费了 api/ 中的接口(如AdditionalWebFilter、TemplateHeadProcessor); extensionPointName声明它挂在哪个扩展点上(如reactive-notifier、search-engine、template-head-processor、attachment-handler、comment-subject),扩展点的注册元数据见 extensionpoint-definitions.yaml;- 因此,当你在 api/ 中新增"扩展点接口",通常要同步做三件事:① 在 api/ 完成契约类型;② 在 extensions 目录 补
ExtensionDefinition元数据;③ 在ui/侧对齐该扩展点的管理界面或渲染逻辑。这正是 AGENTS.md 强调"更新 extension definitions 时必须 application 与 ui 一起改"的原因。
同目录下还存放着角色模板(role-template-post.yaml、role-template-theme.yaml、role-template-uc-attachment.yaml 等)与 authproviders.yaml、notification.yaml、system-setting.yaml,它们共同构成系统启动时加载的"声明式配置面",与 api/ 中的 Java 契约面互为表里。
评审红线:为什么 API 变更需要显式批准
api/AGENTS.md 的 Boundaries 一节给出了全篇最强的一条治理约束:
Public API changes in
api/require explicit approval — treat them as affecting every plugin built against Halo.
这一条是前面所有约定的收口。在协作实践中,它意味着:
- 涉及 api/ 中公开类型、方法签名、扩展点接口、资源字段的改动,应在动手前先说明动机与替代方案,等待明确批准,而不是直接随功能 PR 一起合并;
- 评审人应把 api/ 变更视为"面向所有未知插件"的破坏性风险来评估,而不仅是当前仓库内的应用与前端能否编译通过;
- 结合仓库根目录 AGENTS.md 的 Cross-Module Rules("Ask before public API changes, new dependencies…"),这类变更通常还需要与版本号策略联动——当前仓库版本号
2.26.0-SNAPSHOT记录在 gradle.properties 中,由platform/application/BOM 与gradle/libs.versions.toml统一约束依赖版本,任何版本能力层面的承诺都应以此为准。
源码级延伸:api/ 里值得深入研读的五个设计骨架
围绕 api/ 的目录与测试,可以再给出几条帮助你快速上手的源码阅读线索(均只涉及查看与理解,不需要改动仓库):
-
自定义资源类型如何定义:从 core/extension/content/Post.java 与
Category.java入手,配合 CategoryTest.java、PostTest.java,可以理解 GVK(GroupVersionKind)、Metadata 注解与校验规则在类型上的写法;测试即最佳使用范例。 -
扩展客户端与查询 DSL:extension 下的
ReactiveExtensionClient、ListOptions、PageRequestImpl配合 ListOptionsTest.java、PageRequestImplTest.java;index/query下的条件类可参考 QueriesTest.java 与 LabelSelectorTest.java。 -
控制器(Reconciler)框架:extension/controller 的
ControllerBuilder用于构建声明式控制器,测试侧 ControllerBuilderTest.java 展示了如何装配与验证。 -
安全过滤器扩展面:security 下的
BeforeSecurityWebFilter、AuthenticationSecurityWebFilter、AnonymousAuthenticationSecurityWebFilter等构成了可插拔的过滤器链;工具侧可参考 HttpSecurityUtilsTest.java。 -
主题与插件扩展点:theme 包的
Finder、TemplateHeadProcessor对应插件主题渲染时的挂载点,plugin 包的SettingFetcher/ReactiveSettingFetcher是插件读取配置的标准入口——它们也是 YAML 中extensionPointName对应契约的源头。
建议按"先读一个资源类型 + 它的测试,再读客户端与查询 DSL,最后看控制器与扩展点"的顺序,从 api/ 的最小闭环快速建立起对整个 Halo 扩展机制的完整认知。
小结
api/ 模块的设计哲学可以概括为三句话:类型在 api、schema 在 application、体验在 ui;加法优先、破坏需批;每一次公开变更都以"影响所有插件"为前提来评估。掌握了 api/AGENTS.md 中 Scope、Commands、Conventions、Boundaries 四节的管理思路,再结合 api/build.gradle 的工程配置与 extensions 目录 的 YAML 声明,你既能为 Halo 贡献契合其扩展机制的新能力,也能在本地或 CI 中稳定复现构建、格式化与测试结果——这才是这套 API 库能够长期作为生态兼容基石的原因所在。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00