mirror of
https://github.com/halo-dev/docs.git
synced 2025-10-19 17:04:09 +00:00
docs: update documentation for Halo 2.17 (#378)
为 [Halo 2.17](https://github.com/halo-dev/halo/releases/tag/v2.17.0) 更新文档。 /kind documentation ```release-note None ```
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: 与自定义模型交互
|
||||
description: 了解如果通过代码的方式操作数据
|
||||
---
|
||||
|
||||
Halo 提供了两个类用于与自定义模型对象交互 `ExtensionClient` 和 `ReactiveExtensionClient`。
|
||||
|
||||
它们提供了对自定义模型对象的增删改查操作,`ExtensionClient` 是阻塞式的用于后台任务如控制器中操作数据,而 `ReactiveExtensionClient` 返回值都是 Mono 或 Flux 是反应式非阻塞的,它们由 [reactor](https://projectreactor.io/) 提供。
|
||||
|
||||
```java
|
||||
public interface ReactiveExtensionClient {
|
||||
|
||||
// 已经过时,建议使用 listBy 或 listAll 代替
|
||||
<E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
|
||||
Comparator<E> comparator);
|
||||
|
||||
// 已经过时,建议使用 listBy 或 listAll 代替
|
||||
<E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
|
||||
Comparator<E> comparator, int page, int size);
|
||||
|
||||
<E extends Extension> Flux<E> listAll(Class<E> type, ListOptions options, Sort sort);
|
||||
|
||||
<E extends Extension> Mono<ListResult<E>> listBy(Class<E> type, ListOptions options,
|
||||
PageRequest pageable);
|
||||
|
||||
/**
|
||||
* Fetches Extension by its type and name.
|
||||
*
|
||||
* @param type is Extension type.
|
||||
* @param name is Extension name.
|
||||
* @param <E> is Extension type.
|
||||
* @return an optional Extension.
|
||||
*/
|
||||
<E extends Extension> Mono<E> fetch(Class<E> type, String name);
|
||||
|
||||
Mono<Unstructured> fetch(GroupVersionKind gvk, String name);
|
||||
|
||||
<E extends Extension> Mono<E> get(Class<E> type, String name);
|
||||
|
||||
/**
|
||||
* Creates an Extension.
|
||||
*
|
||||
* @param extension is fresh Extension to be created. Please make sure the Extension name does
|
||||
* not exist.
|
||||
* @param <E> is Extension type.
|
||||
*/
|
||||
<E extends Extension> Mono<E> create(E extension);
|
||||
|
||||
/**
|
||||
* Updates an Extension.
|
||||
*
|
||||
* @param extension is an Extension to be updated. Please make sure the resource version is
|
||||
* latest.
|
||||
* @param <E> is Extension type.
|
||||
*/
|
||||
<E extends Extension> Mono<E> update(E extension);
|
||||
|
||||
/**
|
||||
* Deletes an Extension.
|
||||
*
|
||||
* @param extension is an Extension to be deleted. Please make sure the resource version is
|
||||
* latest.
|
||||
* @param <E> is Extension type.
|
||||
*/
|
||||
<E extends Extension> Mono<E> delete(E extension);
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
如果你想在插件中根据 name 参数查询获取到 Person 自定义模型的数据,则可以这样写:
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public PersonService {
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
Mono<Person> getPerson(String name) {
|
||||
return client.fetch(Person.class, name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或者使用阻塞式 Client
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public PersonService {
|
||||
private final ExtensionClient client;
|
||||
|
||||
Optional<Person> getPerson(String name) {
|
||||
return client.fetch(Person.class, name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:非阻塞线程中不能调用阻塞式方法。
|
||||
|
||||
我们建议你更多的使用响应式的 `ReactiveExtensionClient` 去替代 `ExtensionClient`。
|
||||
|
||||
### 查询
|
||||
|
||||
`ReactiveExtensionClient` 提供了两个方法用于查询数据,`listBy` 和 `listAll`。
|
||||
|
||||
`listBy` 方法用于分页查询数据,`listAll` 方法用于查询所有数据,它们都需要一个 `ListOptions` 参数,用于传递查询条件:
|
||||
|
||||
```java
|
||||
public class ListOptions {
|
||||
private LabelSelector labelSelector;
|
||||
private FieldSelector fieldSelector;
|
||||
}
|
||||
```
|
||||
|
||||
其中 `LabelSelector` 用于传递标签查询条件,`FieldSelector` 用于传递字段查询条件。
|
||||
|
||||
`FieldSelector` 支持比自动生成的 APIs 中更多的查询条件,可以通过 `run.halo.app.extension.index.query.QueryFactory` 来构建。
|
||||
|
||||
```java
|
||||
FieldSelector.of(QueryFactory.and(
|
||||
QueryFactory.equal("name", "test"),
|
||||
QueryFactory.equal("age", 18)
|
||||
))
|
||||
```
|
||||
|
||||
支持的查询条件如下:
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
| ---------------------------- | ---------------- | ----------------------------------------------------------------------------- |
|
||||
| equal | 等于 | equal("name", "test"), name 是字段名,test 是字段值 |
|
||||
| equalOtherField | 等于其他字段 | equalOtherField("name", "otherName"), name 是字段名,otherName 是另一个字段名 |
|
||||
| notEqual | 不等于 | notEqual("name", "test") |
|
||||
| notEqualOtherField | 不等于其他字段 | notEqualOtherField("name", "otherName") |
|
||||
| greaterThan | 大于 | greaterThan("age", 18) |
|
||||
| greaterThanOtherField | 大于其他字段 | greaterThanOtherField("age", "otherAge") |
|
||||
| greaterThanOrEqual | 大于等于 | greaterThanOrEqual("age", 18) |
|
||||
| greaterThanOrEqualOtherField | 大于等于其他字段 | greaterThanOrEqualOtherField("age", "otherAge") |
|
||||
| lessThan | 小于 | lessThan("age", 18) |
|
||||
| lessThanOtherField | 小于其他字段 | lessThanOtherField("age", "otherAge") |
|
||||
| lessThanOrEqual | 小于等于 | lessThanOrEqual("age", 18) |
|
||||
| lessThanOrEqualOtherField | 小于等于其他字段 | lessThanOrEqualOtherField("age", "otherAge") |
|
||||
| in | 在范围内 | in("age", 18, 19, 20) |
|
||||
| and | 且 | and(equal("name", "test"), equal("age", 18)) |
|
||||
| or | 或 | or(equal("name", "test"), equal("age", 18)) |
|
||||
| between | 在范围内 | between("age", 18, 20), 包含 18 和 20 |
|
||||
| betweenExclusive | 在范围内 | betweenExclusive("age", 18, 20), 不包含 18 和 20 |
|
||||
| betweenLowerExclusive | 在范围内 | betweenLowerExclusive("age", 18, 20), 不包含 18,包含 20 |
|
||||
| betweenUpperExclusive | 在范围内 | betweenUpperExclusive("age", 18, 20), 包含 18,不包含 20 |
|
||||
| startsWith | 以指定字符串开头 | startsWith("name", "test") |
|
||||
| endsWith | 以指定字符串结尾 | endsWith("name", "test") |
|
||||
| contains | 包含指定字符串 | contains("name", "test") |
|
||||
| all | 指定字段的所有值 | all("age") |
|
||||
|
||||
在 `FieldSelector` 中使用的所有字段都必须添加为索引,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 [自定义模型使用索引](./extension.md#using-indexes)。
|
||||
|
||||
可以通过 `and` 和 `or` 方法组合和嵌套查询条件:
|
||||
|
||||
```java
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.greaterThan;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.or;
|
||||
|
||||
Query query = and(
|
||||
or(equal("dept", "A"), equal("dept", "B")),
|
||||
or(equal("age", "19"), equal("age", "18"))
|
||||
);
|
||||
FieldSelector.of(query);
|
||||
```
|
||||
|
||||
### 排序
|
||||
|
||||
`listBy` 和 `listAll` 方法都支持传递 `Sort` 参数,用于传递排序条件。
|
||||
|
||||
```java
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
Sort.by(Sort.Order.asc("metadata.name"))
|
||||
```
|
||||
|
||||
通过 `Sort.by` 方法可以构建排序条件,`Sort.Order` 用于指定排序字段和排序方式,`asc` 表示升序,`desc` 表示降序。
|
||||
|
||||
排序中使用的字段必须是添加为索引的字段,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 [自定义模型使用索引](./extension.md#using-indexes)。
|
||||
|
||||
### 分页
|
||||
|
||||
`listBy` 方法支持传递 `PageRequest` 参数,用于传递分页条件。
|
||||
|
||||
```java
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
|
||||
PageRequestImpl.of(1, 10);
|
||||
|
||||
PageRequestImpl.of(1, 10, Sort.by(Sort.Order.asc("metadata.name"));
|
||||
|
||||
PageRequestImpl.ofSize(10);
|
||||
```
|
||||
|
||||
通过 `PageRequestImpl.of` 方法可以构建分页条件,具有两个参数的方法用于指定页码和每页数量,具有三个参数的方法用于指定页码、每页数量和排序条件。
|
||||
|
||||
`ofSize` 方法用于指定每页数量,页码默认为 1。
|
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Web 过滤器
|
||||
description: 为 Web 请求提供过滤器扩展点,可用于对请求进行拦截、修改等操作。
|
||||
---
|
||||
|
||||
在现代的 Web 应用开发中,过滤器(Filter)是一个非常重要的概念。你可以使用 `run.halo.app.security.AdditionalWebFilter` 在服务器处理请求之前或之后执行特定的任务。
|
||||
|
||||
通过实现这个接口,开发者可以自定义过滤逻辑,用于处理进入和离开应用程序的 HTTP 请求和响应。
|
||||
|
||||
AdditionalWebFilter 能做什么?
|
||||
|
||||
1. 认证与授权: AdditionalWebFilter 可以用来检查用户是否登录,或者是否有权限访问某个资源。
|
||||
2. 日志记录与审计: 在请求处理之前或之后记录日志,帮助了解应用程序的使用情况。
|
||||
3. 请求重构: 修改请求数据,例如添加、删除或修改请求头或请求参数。
|
||||
4. 响应处理: 修改响应,例如设置通用的响应头。
|
||||
5. 性能监控: 记录处理请求所需的时间,用于性能分析。
|
||||
6. 异常处理: 统一处理请求过程中抛出的异常。
|
||||
7. ......
|
||||
|
||||
## 使用示例
|
||||
|
||||
以下是一个使用 `AdditionalWebFilter` 来拦截 `/login` 请求实现用户名密码登录的示例:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
|
||||
final ServerWebExchangeMatcher requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login");
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||
return this.requiresAuthenticationMatcher.matches(exchange)
|
||||
.filter((matchResult) -> {
|
||||
return matchResult.isMatch();
|
||||
}).flatMap((matchResult) -> {
|
||||
return this.authenticationConverter.convert(exchange);
|
||||
}).switchIfEmpty(chain.filter(exchange)
|
||||
.then(Mono.empty()))
|
||||
.flatMap((token) -> {
|
||||
return this.authenticate(exchange, chain, token);
|
||||
}).onErrorResume(AuthenticationException.class, (ex) -> {
|
||||
return this.authenticationFailureHandler.onAuthenticationFailure(new WebFilterExchange(exchange, chain), ex);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return SecurityWebFiltersOrder.FORM_LOGIN.getOrder();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. `filter` 方法中的 `chain.filter(exchange)` 表示继续执行后续的过滤器,如果不调用这个方法,请求将不会继续执行后续的过滤器或目标处理程序。
|
||||
2. `getOrder` 方法用于指定过滤器的执行顺序,比如 `SecurityWebFiltersOrder.FORM_LOGIN.getOrder()` 表示在 Spring Security 的表单登录过滤器之前执行,参考:[SecurityWebFiltersOrder](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java)。
|
||||
|
||||
`AdditionalWebFilter` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: additional-webfilter
|
||||
spec:
|
||||
className: run.halo.app.security.AdditionalWebFilter
|
||||
displayName: AdditionalWebFilter
|
||||
type: MULTI_INSTANCE
|
||||
description: "Contract for interception-style, chained processing of Web requests that may be used to
|
||||
implement cross-cutting, application-agnostic requirements such as security, timeouts, and others."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `additional-webfilter`。
|
||||
|
||||
以下是一些可以参考的项目示例:
|
||||
|
||||
- [OAuth2 第三方登录插件](https://github.com/halo-sigs/plugin-oauth2)
|
||||
- [Halo 用户名密码登录](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java)
|
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: 附件存储
|
||||
description: 为附件存储方式提供的扩展点,可用于自定义附件存储方式。
|
||||
---
|
||||
附件存储策略扩展点支持扩展附件的上传和存储方式,如将附件存储到第三方云存储服务中。
|
||||
|
||||
扩展点接口如下:
|
||||
|
||||
```java
|
||||
public interface AttachmentHandler extends ExtensionPoint {
|
||||
|
||||
Mono<Attachment> upload(UploadContext context);
|
||||
|
||||
Mono<Attachment> delete(DeleteContext context);
|
||||
|
||||
default Mono<URI> getSharedURL(Attachment attachment,
|
||||
Policy policy,
|
||||
ConfigMap configMap,
|
||||
Duration ttl) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
default Mono<URI> getPermalink(Attachment attachment,
|
||||
Policy policy,
|
||||
ConfigMap configMap) {
|
||||
return Mono.empty();
|
||||
}
|
||||
```
|
||||
|
||||
- `upload` 方法用于上传附件,返回值为 `Mono<Attachment>`,其中 `Attachment` 为上传成功后的附件对象。
|
||||
- `delete` 方法用于删除附件,返回值为 `Mono<Attachment>`,其中 `Attachment` 为删除后的附件对象。
|
||||
- `getSharedURL` 方法用于获取附件的共享链接,返回值为 `Mono<URI>`,其中 `URI` 为附件的共享链接。
|
||||
- `getPermalink` 方法用于获取附件的永久链接,返回值为 `Mono<URI>`,其中 `URI` 为附件的永久链接。
|
||||
|
||||
`AttachmentHandler` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: attachment-handler
|
||||
spec:
|
||||
className: run.halo.app.core.extension.attachment.endpoint.AttachmentHandler
|
||||
displayName: AttachmentHandler
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provide extension points for attachment storage strategies"
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `attachment-handler`。
|
||||
|
||||
可以参考以下项目示例:
|
||||
|
||||
- [S3 对象存储协议的存储插件](https://github.com/halo-dev/plugin-s3)
|
||||
- [阿里云 OSS 的存储策略插件](https://github.com/halo-sigs/plugin-alioss)
|
||||
- [又拍云 OSS 的存储策略](https://github.com/AirboZH/plugin-uposs)
|
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: 认证安全过滤器
|
||||
description: 提供 Security WebFilter 扩展点,插件可实现自定义认证逻辑,例如:用户名密码认证,JWT 认证,匿名认证。
|
||||
---
|
||||
|
||||
此前,Halo 提供了 AdditionalWebFilter 作为扩展点供插件扩展认证相关的功能。但是近期我们明确了 AdditionalWebFilter
|
||||
的使用用途,故不再作为认证的扩展点。
|
||||
|
||||
目前,Halo 提供了三种认证扩展点:表单登录认证、普通认证和匿名认证。
|
||||
|
||||
## 表单登录(FormLogin)
|
||||
|
||||
示例如下:
|
||||
|
||||
```java
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.security.FormLoginSecurityWebFilter;
|
||||
|
||||
@Component
|
||||
public class MyFormLoginSecurityWebFilter implements FormLoginSecurityWebFilter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
// Do your logic here
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 普通认证(Authentication)
|
||||
|
||||
示例如下:
|
||||
|
||||
```java
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.security.AuthenticationSecurityWebFilter;
|
||||
|
||||
@Component
|
||||
public class MyAuthenticationSecurityWebFilter implements AuthenticationSecurityWebFilter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
// Do your logic here
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 匿名认证(Anonymous Authentication)
|
||||
|
||||
示例如下:
|
||||
|
||||
```java
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.security.AnonymousAuthenticationSecurityWebFilter;
|
||||
|
||||
@Component
|
||||
public class MyAnonymousAuthenticationSecurityWebFilter
|
||||
implements AnonymousAuthenticationSecurityWebFilter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
// Do your logic here
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们在实现扩展点的时候需要注意:如果当前请求不满足认证条件,请一定要调用 `chain.filter(exchange)`,给其他 filter 留下机会。
|
||||
|
||||
后续会根据需求实现其他认证相关的扩展点。
|
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: 评论主体展示
|
||||
description: 用于在管理端评论列表中展示评论的主体内容。
|
||||
---
|
||||
|
||||
评论主体扩展点用于在管理端评论列表中展示评论的主体内容,评论自定义模型是 Halo 主应用提供的,如果你需要使用评论自定义模型,那么评论会统一
|
||||
展示在管理后台的评论列表中,这时就需要通过评论主体扩展点来展示评论的主体内容便于跳转到对应的页面,如果没有实现该扩展点,那么评论列表中对应的评论的主体会显示为“未知”。
|
||||
|
||||
```java
|
||||
public interface CommentSubject<T extends Extension> extends ExtensionPoint {
|
||||
|
||||
Mono<T> get(String name);
|
||||
|
||||
default Mono<SubjectDisplay> getSubjectDisplay(String name) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
boolean supports(Ref ref);
|
||||
|
||||
record SubjectDisplay(String title, String url, String kindName) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `get` 方法用于获取评论主体,参数 `name` 是评论主体的自定义模型对象的名称,返回值为 `Mono<T>`,其中 `T` 为评论主体对象,它是使用评论的那个自定义模型。
|
||||
- `getSubjectDisplay` 方法用于获取评论主体的展示信息,返回值为 `Mono<SubjectDisplay>`,其中 `SubjectDisplay` 为评论主体的展示信息,包含标题、链接和类型名称,用于在主题端展示评论主体的信息。
|
||||
- `supports` 方法用于判断是否支持该评论主体,返回值为 `boolean`,如果支持则返回 `true`,否则返回 `false`。
|
||||
|
||||
实现该扩展点后,评论列表会通过 `get` 方法将主体的自定义模型对象带到评论列表中,可以配置前端的扩展点来决定如何展示评论主体的信息,参考:[UI 评论来源显示](../../ui/extension-points//comment-subject-ref-create.md)
|
||||
|
||||
例如对于文章是支持评论的,所以文章的评论主体扩展点实现如下:
|
||||
|
||||
```java
|
||||
public class PostCommentSubject implements CommentSubject<Post> {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
private final ExternalLinkProcessor externalLinkProcessor;
|
||||
|
||||
@Override
|
||||
public Mono<Post> get(String name) {
|
||||
return client.fetch(Post.class, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SubjectDisplay> getSubjectDisplay(String name) {
|
||||
return get(name)
|
||||
.map(post -> {
|
||||
var url = externalLinkProcessor
|
||||
.processLink(post.getStatusOrDefault().getPermalink());
|
||||
return new SubjectDisplay(post.getSpec().getTitle(), url, "文章");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Ref ref) {
|
||||
Assert.notNull(ref, "Subject ref must not be null.");
|
||||
GroupVersionKind groupVersionKind =
|
||||
new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind());
|
||||
return GroupVersionKind.fromExtension(Post.class).equals(groupVersionKind);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`CommentSubject` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: comment-subject
|
||||
spec:
|
||||
className: run.halo.app.content.comment.CommentSubject
|
||||
displayName: CommentSubject
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provide extension points for comment subject display"
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `comment-subject`。
|
||||
|
||||
可以参考其他使用该扩展点的项目示例:
|
||||
|
||||
- [Halo 自定义页面评论主体](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/content/comment/SinglePageCommentSubject.java)
|
||||
- [瞬间的评论主体](https://github.com/halo-sigs/plugin-moments/blob/096b1b3e4a2ca44b6f858ba1181b62eeff64a139/src/main/java/run/halo/moments/MomentCommentSubject.java#L25)
|
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 评论组件
|
||||
description: 用于自定义评论组件,可在主题端使用其他评论组件。
|
||||
---
|
||||
评论组件扩展点用于自定义主题端使用的评论组件,Halo 通过插件提供了一个默认的评论组件,如果你需要使用其他的评论组件,那么可以通过实现该扩展点来自定义评论组件。
|
||||
|
||||
```java
|
||||
public interface CommentWidget extends ExtensionPoint {
|
||||
|
||||
String ENABLE_COMMENT_ATTRIBUTE = CommentWidget.class.getName() + ".ENABLE";
|
||||
|
||||
void render(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler);
|
||||
}
|
||||
```
|
||||
|
||||
其中 `render` 方法用于在主题端模板中渲染评论组件,参数:
|
||||
|
||||
- `context` 为模板上下文,包含执行模板的上下文:变量、模板数据等。
|
||||
- 参数 `tag` 为 `<halo:comment />` 标签它包含元素的名称及其属性
|
||||
- `structureHandler` 是一个特殊的对象,它允许 `CommentWidget` 向引擎发出指令,指示模板引擎应根据处理器的执行而执行哪些操作。
|
||||
|
||||
`CommentWidget` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: comment-widget
|
||||
spec:
|
||||
className: run.halo.app.theme.dialect.CommentWidget
|
||||
displayName: CommentWidget
|
||||
type: SINGLETON
|
||||
description: "Provides an extension point for the comment widget on the theme-side."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `comment-widget`。
|
||||
|
||||
参考:[Thymeleaf IElementTagProcessor 文档](https://www.thymeleaf.org/doc/tutorials/3.1/extendingthymeleaf.html#element-tag-processors-ielementtagprocessor)。
|
||||
|
||||
参考:[Halo 默认评论组件的实现](https://github.com/halo-dev/plugin-comment-widget/blob/main/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java)。
|
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: 扩展点
|
||||
description: Halo 服务端为插件提供的扩展点接口
|
||||
---
|
||||
|
||||
术语:
|
||||
|
||||
- 扩展点
|
||||
- 由 Halo 定义的用于添加特定功能的接口。
|
||||
- 扩展点应该在服务的核心功能和它所认为的集成之间的交叉点上。
|
||||
- 扩展点是对服务的扩充,但不是影响服务的核心功能:区别在于,如果没有其核心功能,服务就无法运行,而扩展点对于特定的配置可能至关重要该服务最终是可选的。
|
||||
- 扩展点应该小且可组合,并且在相互配合使用时,可为 Halo 提供比其各部分总和更大的价值。
|
||||
- 扩展
|
||||
- 扩展点的一种具体实现。
|
||||
|
||||
使用 Halo 扩展点的必要步骤是:
|
||||
|
||||
1. 实现扩展点接口,然后标记上 `@Component` 注解。
|
||||
2. 对该扩展点的实现类进行 `ExtensionDefinition` 自定义模型对象的声明。
|
||||
|
||||
例如: 实现一个通知器的扩展,首先 `implements` ReactiveNotifier 扩展点接口:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class EmailNotifier implements ReactiveNotifier {
|
||||
@Override
|
||||
public Mono<Void> notify(NotificationContext context) {
|
||||
// Send notification
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后声明一个 `ExtensionDefinition` 自定义模型对象:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionDefinition
|
||||
metadata:
|
||||
name: halo-email-notifier # 指定一个扩展定义的名称
|
||||
spec:
|
||||
# 扩展的全限定类名
|
||||
className: run.halo.app.notification.EmailNotifier
|
||||
# 所实现的扩展点定义的自定义模型对象名称
|
||||
extensionPointName: reactive-notifier
|
||||
# 扩展名称用于展示给用户
|
||||
displayName: "EmailNotifier"
|
||||
# 扩展的简要描述,用于让用户了解该扩展的作用
|
||||
description: "Support sending notifications to users via email"
|
||||
```
|
||||
|
||||
:::tip Note
|
||||
单实例或多实例的扩展点需要声明对应的 `ExtensionPointDefinition` 自定义模型对象被称之为扩展点定义,用于描述该扩展点的信息,例如:扩展点的名称、描述、扩展点的类型等。
|
||||
|
||||
单实例或多实例扩展点的实现也必须声明一个对应的 `ExtensionDefinition` 自定义模型对象被称之为扩展定义,用于描述该扩展的信息,例如:扩展的名称、描述、对应扩展点的对象名称等。
|
||||
:::
|
||||
|
||||
关于如何在插件中声明自定义模型对象请参考:[自定义模型](../../server/extension.md#declare-extension-object)
|
||||
|
||||
以下是目前已支持的扩展点列表:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
|
||||
<DocCardList />
|
||||
```
|
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: 通知器
|
||||
description: 为以何种方式向用户发送通知提供的扩展点。
|
||||
---
|
||||
|
||||
通知器扩展点是用于扩展为 Halo 通知系统提供更多通知方式的扩展点,例如:邮件、短信、WebHook 等。
|
||||
|
||||
```java
|
||||
public interface ReactiveNotifier extends ExtensionPoint {
|
||||
|
||||
Mono<Void> notify(NotificationContext context);
|
||||
}
|
||||
```
|
||||
|
||||
`notify` 方法用于发送通知,参数:context 为通知上下文,包含通知的内容、接收者、通知配置等信息。
|
||||
|
||||
除了实现该扩展点并声明 `ExtensionDefinition` 自定义模型对象外,还需要声明一个 `NotifierDescriptor` 自定义模型对象用于描述通知器,例如:
|
||||
|
||||
```yaml
|
||||
apiVersion: notification.halo.run/v1alpha1
|
||||
kind: NotifierDescriptor
|
||||
metadata:
|
||||
name: default-email-notifier
|
||||
spec:
|
||||
displayName: '邮件通知'
|
||||
description: '通过邮件将通知发送给用户'
|
||||
notifierExtName: 'halo-email-notifier'
|
||||
senderSettingRef:
|
||||
name: 'notifier-setting-for-email'
|
||||
group: 'sender'
|
||||
#receiverSettingRef:
|
||||
# name: ''
|
||||
# group: ''
|
||||
```
|
||||
|
||||
- `notifierExtName` 为通知器扩展的自定义模型对象名称
|
||||
- `senderSettingRef` 用于声明通知器的发送者配置,例如:邮件通知器的发送者配置为:SMTP 服务器地址、端口、用户名、密码等,如果没有可以不配置,参考:[表单定义](../../../../form-schema.md)
|
||||
- `name` 为发送者配置的名称,它是一个 `Setting` 自定义模型对象的名称。
|
||||
- `group` 用于引用到一个具体的配置 Schema 组,它是一个 `Setting` 自定义模型对象中描述的 `formSchema` 的 `group`,由于 `Setting` 可以声明多个配置分组但通知器的发送者配置只能有在一个组,因此需要指定一个组。
|
||||
- `receiverSettingRef` 用于声明通知器的接收者配置,例如:邮件通知器的接收者配置为:接收者邮箱地址,如果没有可以不配置,`name` 和 `group` 配置同 `senderSettingRef`。
|
||||
|
||||
当配置了 `senderSettingRef` 后,触发通知时 `notify` 方法的 `context` 参数中会包含 `senderConfig` 即为发送者配置的值,`receiverConfig` 同理。
|
||||
|
||||
`ReactiveNotifier` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: reactive-notifier
|
||||
spec:
|
||||
className: run.halo.app.notification.ReactiveNotifier
|
||||
displayName: Notifier
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provides a way to extend the notifier to send notifications to users."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-notifier`。
|
||||
|
||||
使用案例可以参考:[Halo 邮件通知器](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/notification/EmailNotifier.java)
|
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: 主题端文章内容处理
|
||||
description: 提供扩展主题端文章内容处理的方法,干预文章内容的渲染。
|
||||
---
|
||||
|
||||
主题端文章内容处理扩展点用于干预文章内容的渲染,例如:在文章内容中添加广告、添加版权信息等。
|
||||
|
||||
```java
|
||||
public interface ReactivePostContentHandler extends ExtensionPoint {
|
||||
|
||||
Mono<PostContentContext> handle(@NonNull PostContentContext postContent);
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
class PostContentContext {
|
||||
private Post post;
|
||||
private String content;
|
||||
private String raw;
|
||||
private String rawType;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`handle` 方法用于处理文章内容,参数 `postContent` 为文章内容上下文,包含文章自定义模型对象、文章 html 内容、原始内容、原始内容类型等信息。
|
||||
|
||||
`ReactivePostContentHandler` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: reactive-post-content-handler
|
||||
spec:
|
||||
className: run.halo.app.theme.ReactivePostContentHandler
|
||||
displayName: ReactivePostContentHandler
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provides a way to extend the post content to be displayed on the theme-side."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-post-content-handler`。
|
||||
|
||||
使用案例可以参考:[WebP Cloud 插件](https://github.com/webp-sh/halo-plugin-webp-cloud/blob/a6069dfa78931de0d5b5dfe98fdd18a0da75b09f/src/main/java/se/webp/plugin/WebpCloudPostContentHandler.java#L17)
|
||||
它的作用是处理主题端文章内容中的所有图片的地址,将其替换为一个 WebP Cloud 的代理地址,从而实现文章内容中的图片都使用 WebP 格式。
|
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: 主题端自定义页面内容处理
|
||||
description: 提供扩展主题端自定义页面内容处理的方法,干预自定义页面内容的渲染。
|
||||
---
|
||||
|
||||
主题端自定义页面内容处理扩展点,作用同 [主题端文章内容处理](./post-content.md) 扩展点,只是作用于自定义页面。
|
||||
|
||||
```java
|
||||
public interface ReactiveSinglePageContentHandler extends ExtensionPoint {
|
||||
|
||||
Mono<SinglePageContentContext> handle(@NonNull SinglePageContentContext singlePageContent);
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
class SinglePageContentContext {
|
||||
private SinglePage singlePage;
|
||||
private String content;
|
||||
private String raw;
|
||||
private String rawType;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ReactiveSinglePageContentHandler` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: reactive-singlepage-content-handler
|
||||
spec:
|
||||
className: run.halo.app.theme.ReactiveSinglePageContentHandler
|
||||
displayName: ReactiveSinglePageContentHandler
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provides a way to extend the single page content to be displayed on the theme-side."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-singlepage-content-handler`。
|
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 主题端 HTML Head 标签处理
|
||||
description: 提供扩展主题端 HTML 页面中的 Head 标签内容处理的方法,干预 HTML 页面的 Head 标签内容。
|
||||
---
|
||||
|
||||
主题端 HTML Head 标签处理扩展点的作用是干预 HTML 页面中的 Head 标签内容,可以添加自定义的 CSS、JS 及 meta 标签等,以满足特定的定制化需求。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **添加自定义样式或脚本**:在 HTML Head 中插入额外的 CSS 文件或 JavaScript 脚本文件,以增强页面的交互性或样式。
|
||||
- **定制 Meta 标签**:为特定页面添加或修改 meta 标签,如描述、作者、关键词等,以提高 SEO 和页面信息的完整性。
|
||||
- **引入第三方库**:引入第三方库(如 Google Fonts、Font Awesome 等),以满足页面的特殊功能或风格需求。
|
||||
- **定制 Open Graph 等社交媒体标签**:为社交媒体分享优化页面标签内容。
|
||||
|
||||
## 扩展点定义
|
||||
|
||||
主题端 HTML Head 标签处理的扩展点定义为 `TemplateHeadProcessor`,对应的 `ExtensionPoint` 类型为 `MULTI_INSTANCE`,即可以有多个实现类。
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface TemplateHeadProcessor extends ExtensionPoint {
|
||||
|
||||
Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler);
|
||||
}
|
||||
```
|
||||
|
||||
`TemplateHeadProcessor` 对应的 `ExtensionPointDefinition` 资源描述如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: template-head-processor
|
||||
spec:
|
||||
className: run.halo.app.theme.dialect.TemplateHeadProcessor
|
||||
displayName: TemplateHeadProcessor
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provides a way to extend the head tag content in the theme-side HTML page."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `template-head-processor`。
|
||||
|
||||
## 示例实现
|
||||
|
||||
以下是一个简单的 TemplateHeadProcessor 插件实现示例:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CustomHeadProcessor implements TemplateHeadProcessor {
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
// 添加自定义 CSS 文件
|
||||
model.add(context.createStandaloneElementTag("link",
|
||||
"rel", "stylesheet",
|
||||
"href", "/custom/styles.css"));
|
||||
|
||||
// 添加自定义 Meta 标签
|
||||
model.add(context.createStandaloneElementTag("meta",
|
||||
"name", "author",
|
||||
"content", "Your Name"));
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
声明 ExtensionDefinition 自定义模型对象时对应的 extensionPointName 为 template-head-processor。
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionDefinition
|
||||
metadata:
|
||||
name: custom-head-extension
|
||||
spec:
|
||||
extensionPointName: template-head-processor
|
||||
className: com.example.CustomHeadProcessor
|
||||
displayName: "Custom Head Extension"
|
||||
description: "Adds custom CSS and meta tags to the head section."
|
||||
```
|
||||
|
||||
## 使用此扩展点的插件
|
||||
|
||||
- [集成 highlight.js 为文章提供代码块高亮渲染](https://github.com/halo-sigs/plugin-highlightjs)
|
||||
- [集成 lightgallery.js,支持在内容页面放大显示图片](https://github.com/halo-sigs/plugin-lightgallery)
|
||||
- [Halo 2.0 对 Umami 的集成](https://github.com/halo-sigs/plugin-umami)
|
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: 用户名密码认证管理器
|
||||
description: 提供扩展用户名密码的身份验证的方法
|
||||
---
|
||||
|
||||
用户名密码认证管理器扩展点用于替换 Halo 默认的用户名密码认证管理器实现,例如:使用第三方的身份验证服务,一个例子是 LDAP。
|
||||
|
||||
```java
|
||||
public interface UsernamePasswordAuthenticationManager extends ExtensionPoint {
|
||||
Mono<Authentication> authenticate(Authentication authentication);
|
||||
}
|
||||
```
|
||||
|
||||
`UsernamePasswordAuthenticationManager` 对应的 `ExtensionPointDefinition` 如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: username-password-authentication-manager
|
||||
spec:
|
||||
className: run.halo.app.security.authentication.login.UsernamePasswordAuthenticationManager
|
||||
displayName: Username password authentication manager
|
||||
type: SINGLETON
|
||||
description: "Provides a way to extend the username password authentication."
|
||||
```
|
||||
|
||||
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `username-password-authentication-manager`。
|
||||
|
||||
可以参考的实现示例 [TOTP 认证](https://github.com/halo-dev/halo/blob/86e688a15d05c084021b6ba5e75d56a8813c3813/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java#L84)
|
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: 自定义模型
|
||||
description: 了解什么是自定义模型及如何创建
|
||||
---
|
||||
|
||||
Halo 自定义模型主要参考自 [Kubernetes CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) 。自定义模型遵循 [OpenAPI v3](https://spec.openapis.org/oas/v3.1.0)。设计目的在于提供一种灵活可扩展的数据存储和使用方式,便于为插件提供自定义数据支持。
|
||||
比如某插件需要存储自定义数据,同时也想读取和操作自定义数据。更多细节请参考 [自定义模型设计](https://github.com/halo-dev/rfcs/tree/main/extension)。
|
||||
|
||||
一个典型的自定义模型 `Java` 代码示例如下:
|
||||
|
||||
```java
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupKind;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@GVK(group = "my-plugin.halo.run",
|
||||
version = "v1alpha1",
|
||||
kind = "Person",
|
||||
plural = "persons",
|
||||
singular = "person")
|
||||
public class Person extends AbstractExtension {
|
||||
|
||||
@Schema(requireMode = Schema.RequireMode.REQUIRED)
|
||||
private Spec spec;
|
||||
|
||||
@Schema(name="PersonSpec")
|
||||
public static class Spec {
|
||||
@Schema(description = "The description on name field", maxLength = 100)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "The description on age field", maximum = "150", minimum = "0")
|
||||
private Integer age;
|
||||
|
||||
@Schema(description = "The description on gender field")
|
||||
private Gender gender;
|
||||
|
||||
private Person otherPerson;
|
||||
}
|
||||
|
||||
public enum Gender {
|
||||
MALE, FEMALE,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要创建一个自定义模型需要三步:
|
||||
|
||||
1. 创建一个类继承 `run.halo.app.extension.AbstractExtension`。
|
||||
2. 使用 `GVK` 注解。
|
||||
3. 在插件 `start()` 生命周期方法中注册自定义模型:
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
schemeManager.register(Person.class);
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 释义
|
||||
|
||||
- @GVK:此注解标识该类为一个自定义模型,同时必须继承 `AbstractExtension`。
|
||||
- kind:表示自定义模型所表示的 REST 资源。
|
||||
- group:表示一组公开的资源,通常采用域名形式,Halo 项目保留使用空组和任何以 `*.halo.run` 结尾的组名供其单独使用。
|
||||
选择群组名称时,我们建议选择你的群组或组织拥有的子域,例如 `widget.mycompany.com`,而这里提到的公开并不是指你的自定义资源可以被任何人访问,
|
||||
而是指你的自定义模型对象可以被以 APIs 的形式访问。
|
||||
- version:API 的版本,它与 group 组合使用为 `apiVersion=GROUP/VERSION`,例如`api.halo.run/v1alpha1`。
|
||||
- singular: 资源的单数名称,这允许客户端不透明地处理复数和单数,必须全部小写,通常是将 `kind` 的值转换为小写作为 `singular` 的值。
|
||||
- plural: 资源的复数名称,自定义资源在 `/apis/<group>/<version>/.../<plural>` 下提供,必须为全部小写,通常是将 `kind` 的值转换为小写并转为复数形式作为 `plural` 的值。
|
||||
- @Schema:属性校验注解,会在创建/修改资源前对资源进行简单校验,参考 [schema-validator](https://www.openapi4j.org/schema-validator.html)。
|
||||
:::
|
||||
|
||||
一个自定义模型通常包括以下几个部分:
|
||||
|
||||
- `apiVersion`: 用于标识自定义模型的 API 版本,它由 `GVK` 注解的 `group` 和 `version` 组合而成。
|
||||
- `kind`: 用于标识自定义模型的类型,它由 `GVK` 注解的 `kind` 声明。
|
||||
- `metadata`: 用于标识自定义模型的元数据:
|
||||
- `name`: 用于标识自定义模型的名称。
|
||||
- `creationTimestamp`: 用于标识自定义模型的创建时间,无法修改,只在创建时自动生成。
|
||||
- `version`: 用于标识自定义模型的数据乐观锁版本,无法修改,由更新时自动填充,如果更新时指定了 `version` 且与当前 `version` 不一致则会更新失败。
|
||||
- `deletionTimestamp`: 用于标识自定义模型的删除时间,表示此自定义模型对象被声明为删除,此时仍然可以通过 API 访问到此对象,参考 [自定义模型对象生命周期](../../basics/framework.md#extension-lifecycle)
|
||||
- `finalizers`: 用于标识终结器,它是一个字符串集合,用于标识自定义模型对象是否可回收,参考 [自定义模型对象生命周期](../../basics/framework.md#extension-lifecycle)
|
||||
- `labels`: 用于标识自定义模型的标签,它是一个字符串键值对集合,用于标识自定义模型对象的标签,可以通过标签来查询自定义模型对象。
|
||||
- `annotations`: 用于存放扩展信息,它是一个字符串键值对集合,用于存放自定义模型对象的扩展信息。
|
||||
- `spec`: 用于声明自定义模型对象的期望状态,它是声明式的,用户只需要声明期望状态,实际状态由具体的控制器来维护,最终达到用户期望的状态。
|
||||
- `status`: 用于描述自定义模型对象资源状态的变化,和一些实际状态。
|
||||
|
||||
其中 `apiVersion`、`kind`、`metadata`都包含在了 AbstractExtension 类中,所以我们只需要关注 `spec` 和 `status` 即可,参考:[Halo 架构概览之自定义模型](../../basics/framework.md#extension)
|
||||
|
||||
## 声明自定义模型对象 {#declare-extension-object}
|
||||
|
||||
有了自定义模型后可以通过在插件项目的 `src/main/resources/extensions` 目录下声明 `yaml` 文件来创建一个自定义模型对象,
|
||||
此目录下的所有自定义模型 `yaml` 都会在插件启动后被创建:
|
||||
|
||||
```yaml
|
||||
apiVersion: my-plugin.halo.run/v1alpha1
|
||||
kind: Person
|
||||
metadata:
|
||||
name: fake-person
|
||||
spec:
|
||||
name: halo
|
||||
age: 18
|
||||
gender: male
|
||||
```
|
||||
|
||||
在该目录下声明自定义模型对象所使用的 `yaml` 文件的文件名是任意的,只根据 `kind` 和 `apiVersion` 来确定自定义模型对象的类型。
|
||||
|
||||
## 命名规范 {#naming-conventions}
|
||||
|
||||
### metadata name {#metadata-name}
|
||||
|
||||
`metadata.name` 它是自定义模型对象的唯一标识名,包含不超过 253 个字符,仅包含小写字母、数字或`-`,以字母或数字开头,以字母或数字结尾。
|
||||
|
||||
### labels
|
||||
|
||||
`labels` 它是一个字符串键值对集合, Key 的基本结构为 `<prefix>/<name>`,完整的 label 键通常包括一个可选的前缀和名称,二者通过斜杠(/)分隔。
|
||||
|
||||
- 前缀(可选):通常是域名的反向表示形式,用于避免键名冲突。例如,halo.run/post-slug
|
||||
- 名称:标识 label 的具体含义,如 post-slug。
|
||||
|
||||
前缀规则:
|
||||
|
||||
- 如果 label 用于特定于一个组织的资源,建议使用一个前缀,如 `plugin.halo.run/plugin-name`。
|
||||
- 前缀必须是一个有效的 DNS 子域名(参考 metadata.name),且最多可包含 253 个字符。
|
||||
- 保留了不带前缀的 label 键以及特定前缀(如 halo.run),因此插件不可使用。
|
||||
|
||||
名称规则:
|
||||
|
||||
- 名称必须是合法的 DNS 标签,最多可包含 63 个字符。
|
||||
- 必须以字母数字字符开头和结尾。
|
||||
- 可以包含 `-`、`.`、`_` 和`字母数字`字符。
|
||||
|
||||
通用规范:
|
||||
|
||||
- 避免使用容易引起混淆或误解的键名。
|
||||
- 尽量保持简洁明了,易于理解。
|
||||
- 使用易于记忆和识别的单词或缩写。
|
||||
|
||||
一致性和清晰性:
|
||||
|
||||
- 在整个项目或组织中保持一致的命名约定。
|
||||
- labels 应直观地反映其代表的信息或用途。
|
||||
- 不要在 labels 中包含敏感信息,例如用户凭据或个人识别信息。
|
||||
|
||||
## 使用索引 {#using-indexes}
|
||||
|
||||
自定义模型虽然带来了很大的灵活性可扩展性,但也引入了查询问题,自定义模型对象存储在数据库中是 `byte[]` 的形式存在的,从而实现不依赖于数据库特性,你可以使用 `MySQL`,`PostgreSQL`,`H2` 等数据库来来作为存储介质,但查询自定义模型对象时无法使用数据库的索引特性,这就导致了查询自定义模型对象的效率问题,Halo 自己实现了一套索引机制来解决这个问题。
|
||||
|
||||
索引是一种存储数据结构,可提供对数据集中字段的高效查找。索引将自定义模型中的字段映射到数据库行,以便在查询特定字段时不需要完整的扫描。查询数据之前,必须对需要查询的字段创建索引。索引可以包含一个或多个字段的值。索引可以包含唯一值或重复值。索引中的值按照索引中的顺序进行排序。
|
||||
|
||||
索引可以提高查询性能,但会占用额外的存储空间,因为它们需要存储索引字段的副本。索引的大小取决于字段的数据类型和索引的类型,因此,创建索引时应该考虑存储成本和性能收益。
|
||||
|
||||
你可以通过以下方式在注册自定义模型时声明索引:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void start() {
|
||||
schemeManager.register(Moment.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.tags")
|
||||
.setIndexFunc(multiValueAttribute(Moment.class, moment -> {
|
||||
var tags = moment.getSpec().getTags();
|
||||
return tags == null ? Set.of() : tags;
|
||||
}))
|
||||
);
|
||||
// more index spec
|
||||
}
|
||||
```
|
||||
|
||||
`IndexSpec` 用于声明索引项,它包含以下属性:
|
||||
|
||||
- name:索引名称,在同一个自定义模型的索引中必须唯一,一般建议使用字段路径作为索引名称,例如 `spec.slug`。
|
||||
- order:对索引值的排序方式,支持 `ASC` 和 `DESC`,默认为 `ASC`。
|
||||
- unique:是否唯一索引,如果为 `true` 则索引值必须唯一,如果创建自定义模型对象时检测到此索引字段有重复值则会创建失败。
|
||||
- indexFunc:索引函数,用于获取索引值,接收当前自定义模型对象,返回一个索引值,索引值必须是字符串任意类型,如果不是字符串类型则需要自己转为字符串,可以使用 `IndexAttributeFactory` 提供的静态方法来创建 `indexFunc`:
|
||||
- `simpleAttribute()`:用于得到一个返回单个值的索引函数,例如 `moment -> moment.getSpec().getSlug()`。
|
||||
- `multiValueAttribute()`:用于得到一个返回多个值的索引函数,例如 `moment -> moment.getSpec().getTags()`。
|
||||
|
||||
当注册自定义模型时声明了索引后,Halo 会在插件启动时构建索引,在构建索引期间插件出于未启动状态。
|
||||
|
||||
Halo 默认会为每个自定义模型建立以下几个索引,因此不需要为下列字段再次声明索引:
|
||||
|
||||
- `metadata.name` 创建唯一索引
|
||||
- `metadata.labels`
|
||||
- `metadata.creationTimestamp`
|
||||
- `metadata.deletionTimestamp`
|
||||
|
||||
创建了索引的字段可以在查询时使用 `fieldSelector` 参数来查询,参考 [自定义模型 APIs](#extension-apis)。
|
||||
|
||||
## 自定义模型 APIs {#extension-apis}
|
||||
|
||||
定义好自定义模型并注册后,会根据 `GVK` 注解自动生成一组 `CRUD` APIs,规则为:
|
||||
`/apis/<group>/<version>/<extension>/{extensionname}/<subextension>`
|
||||
|
||||
对于上述 Person 自定义模型将有以下 APIs:
|
||||
|
||||
```shell
|
||||
# 用于列出所有 Person 自定义模型对象
|
||||
GET /apis/my-plugin.halo.run/v1alpha1/persons
|
||||
|
||||
# 用于查询指定名称更新自定义模型对象
|
||||
PUT /apis/my-plugin.halo.run/v1alpha1/persons/{name}
|
||||
|
||||
# 用于创建自定义模型对象
|
||||
POST /apis/my-plugin.halo.run/v1alpha1/persons
|
||||
|
||||
# 用于根据指定名称删除自定义模型对象
|
||||
DELETE /apis/my-plugin.halo.run/v1alpha1/persons/{name}
|
||||
```
|
||||
|
||||
对于这组自动生成的 `CRUD` APIs,你可以通过定义[控制器](../../basics/framework.md#controller)来完成对数据修改后的业务逻辑处理来满足大部分的场景需求。
|
||||
|
||||
`GET /apis/my-plugin.halo.run/v1alpha1/persons` 这个 API 用于查询自定义模型对象,它支持以下参数:
|
||||
|
||||
- page:页码,从 1 开始。
|
||||
- size:每页数据量,如果不传此参数默认为查询所有。
|
||||
- sort:排序字段,格式为 `字段名,排序方式`,例如 `name,desc`,如果不传此参数默认为按照 `metadata.creationTimestamp` 降序排序,排序使用的字段必须是注册为索引的字段。
|
||||
- labelSelector:标签选择器,格式为 `key=value`,例如 `labelSelector=name=halo`,如果不传此参数默认为查询所有,此标签选择器筛选的是 `metadata.labels`,支持的操作符有 `=`、 `!=`、`!` 和 `存在检查`:
|
||||
- `=` 表示等于,例如 `labelSelector=name=halo` 表示查询 `metadata.labels` 中 `name` 的值等于 `halo` 的自定义模型对象。
|
||||
- `!=` 表示不等于,例如 `labelSelector=name!=halo` 表示查询 `metadata.labels` 中 `name` 的值不等于 `halo`的自定义模型对象。
|
||||
- `!` 表示不存在 key,例如 `labelSelector=!name` 表示查询 `metadata.labels` 不存在 `name` 这个 key 的自定义模型对象。
|
||||
- `存在检查` 表示查询存在 key 的对象,例如 `labelSelector=name` 表示查询 `metadata.labels` 存在 `name` 这个 key 的自定义模型对象。
|
||||
- fieldSelector:字段选择器,格式与 `labelSelector` 类似,但需要确保对应的字段是注册为索引的字段,例如 `fieldSelector=spec.name=slug` 表示查询 `spec.slug` 的值等于 `halo` 的自定义模型对象,支持的操作符有 `=`、`!=` 和 `in`。
|
||||
- `=` 表示等于,例如 `fieldSelector=spec.slug=halo` 表示查询 `spec.slug` 的值等于 `halo` 的自定义模型对象。
|
||||
- `!=` 表示不等于,例如 `fieldSelector=spec.slug!=halo` 表示查询 `spec.slug` 的值不等于 `halo` 的自定义模型对象。
|
||||
- `in` 表示在集合中,例如 `fieldSelector=spec.slug=(halo,halo2)` 表示查询 `spec.slug` 的值在 `halo` 和 `halo2` 中的自定义模型对象。
|
||||
|
||||
这些查询参数是 `AND` 的关系,例如 `page=1&size=10&sort=name,desc&labelSelector=name=halo&fieldSelector=spec.slug=halo` 表示查询 `metadata.labels` 中 `name` 的值等于 `halo` 且 `spec.slug` 的值等于 `halo` 的自定义模型对象,并按照 `name` 字段降序排序,查询第 1 页,每页 10 条数据。
|
||||
|
||||
## 自定义 API {#custom-extension-apis}
|
||||
|
||||
在一些场景下,只有自动生成的 `CRUD` API 往往是不够用的,此时可以通过自定义一些 APIs 来满足功能。
|
||||
|
||||
你可以使用 [Spring Framework Web](https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html) 的 Adaptive 写法来暴露 APIs,不同的是需要添加 `@ApiVersion` 注解,没有此注解的 `Controller` 将被忽略:
|
||||
|
||||
```java
|
||||
@ApiVersion("fake.halo.run/v1alpha1")
|
||||
@RequestMapping("/apples")
|
||||
@RestController
|
||||
public class AppleController {
|
||||
|
||||
@PostMapping("/starting")
|
||||
public void starting() {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当插件被启动时,Halo 将会为此 AppleController 生成一个 API:
|
||||
|
||||
```text
|
||||
/apis/fake.halo.run/v1alpha1/apples/starting
|
||||
```
|
||||
|
||||
但我们**更推荐使用** [Functional Endpoints](https://docs.spring.io/spring-framework/reference/web/webflux-functional.html) 写法来提供 APIs,这是一种轻量级函数式编程写法:
|
||||
|
||||
```java
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), this::getPerson)
|
||||
.GET("/person", accept(APPLICATION_JSON), this::listPeople)
|
||||
.POST("/person", this::createPerson)
|
||||
.add(otherRoute)
|
||||
.build();
|
||||
|
||||
public Mono<ServerResponse> listPeople(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
HTTP 请求通过 HandlerFunction 处理:这是一个接收 ServerRequest 并返回延迟的 ServerResponse(即 `Mono<ServerResponse>`)的函数。
|
||||
请求和响应对象都有不可变的约定,它们提供了对 HTTP 请求和响应的 JDK 8 友好访问。HandlerFunction 相当于基于注解的编程模型中 @RequestMapping 方法的主体。
|
||||
|
||||
传入的请求通过 RouterFunction 路由到一个处理函数:这是一个接收 ServerRequest 并返回延迟的 HandlerFunction(即 `Mono<HandlerFunction>`)的函数。
|
||||
当路由函数匹配时,返回一个处理函数;否则返回一个空的 Mono。RouterFunction 相当于 `@RequestMapping` 注解,但主要区别在于路由函数不仅提供数据,还提供行为。
|
||||
|
||||
ServerRequest 和 ServerResponse 是不可变的接口,它们提供了对 HTTP 请求和响应的 JDK 8 友好访问。请求和响应都针对主体流提供了
|
||||
[Reactive Streams](https://www.reactive-streams.org/) 的背压(back pressure)。请求主体用 Reactor Flux 或 Mono 表示。
|
||||
响应主体可用任何响应式流发布者(Publisher)表示,包括 Flux 和 Mono。
|
||||
更多相关信息,请参见 [Reactor 3 Reference Guide](https://projectreactor.io/docs/core/release/reference/) 和 [Webflux](https://docs.spring.io/spring-framework/reference/web/webflux.html)。
|
||||
|
||||
### 自定义 API 的路由规则 {#route-rules-for-custom-apis}
|
||||
|
||||
自定义模型对象的自定义 APIs 的路由规则建议与自动生成的 CRUD APIs 的路由规则保持一致,这样可以方便用户理解和使用,因此对于自动生成的 CRUD APIs 的路由规则建议遵循以下规则:
|
||||
|
||||
1. 以 `/apis/<group>/<version>/<plural>[/<resourceName>/<subresource>]` 规则组成 APIs。
|
||||
2. 为了与自动生成的 CRUD APIs 区分开避免冲突,我们选择通过不同的 group 来区分,自定义 APIs 的 group 建议遵循以下规则,以便保证唯一性的同时能与自定义模型产生关联:
|
||||
|
||||
- 在 Console 端使用的自定义 APIs 的 group 规则建议为 `console.api.<group>`,例如对于 Person 自定义模型需要额外的在 Console 端使用的自定义 APIs 那么 group 可以定义为 `console.api.my-plugin.halo.run`,则可能的用于查询 Person 列表的 API 的规则应为 `/apis/console.api.my-plugin.halo.run/v1alpha1/persons`。
|
||||
- 在个人中心使用的自定义 APIs 的 group 规则建议为 `uc.api.<group>`,例如 `uc.api.my-plugin.halo.run`。
|
||||
- 为主题端提供的公开的自定义 APIs 的 group 规则建议为 `api.<group>`,例如 `api.my-plugin.halo.run`。
|
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: 为主题提供数据
|
||||
description: 了解如何为主题提供更多获取和使用数据的方法。
|
||||
---
|
||||
|
||||
当你在插件中创建了自己的自定义模型时,你可能需要在主题模板中使用这些数据。或者,你提供一些额外的数据,以便主题可以使用它们,你可以通过创建一个自定义的 `finder` 来实现这一点。
|
||||
|
||||
## 创建一个 Finder
|
||||
|
||||
首先,你需要创建一个 `interface`,并在其中定义你需要提供给主题获取数据的方法,方法的返回值可以是 `Mono` 或 `Flux` 类型,例如:
|
||||
|
||||
```java
|
||||
public interface LinkFinder {
|
||||
Mono<LinkVo> get(String linkName);
|
||||
|
||||
Flux<LinkVo> listAll();
|
||||
}
|
||||
```
|
||||
|
||||
然后写一个实现类,实现这个 `interface`,并在类上添加 `@Finder` 注解,例如:
|
||||
|
||||
```java
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
|
||||
@Finder("myPluginLinkFinder")
|
||||
public class LinkFinderImpl implements LinkFinder {
|
||||
@Override
|
||||
public Mono<LinkVo> get(String linkName) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<LinkVo> listAll() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`@Finder` 注解的值是你在主题中使用的名称,例如,你可以在主题中使用 `myPluginLinkFinder.get('a-link-name')` 来获取数据,`myPluginLinkFinder` 就是你在 `@Finder` 注解中定义的名称。
|
||||
|
||||
## Finder 命名
|
||||
|
||||
为了避免与其他插件的 `finder` 名称冲突,建议在 `@Finder` 注解中添加一个你插件名称的前缀作为 `finder` 名称且名称需要是驼峰式的,不能包含除了 `_` 之外的其他特殊字符。
|
||||
|
||||
例如,你的插件名称是 `my_plugin`,你需要为主题提供一个获取链接的 `finder`,那么你可以这样定义 `@Finder` 注解:
|
||||
|
||||
```java
|
||||
@Finder("myPluginLinkFinder")
|
||||
```
|
||||
|
||||
## 使用 Finder
|
||||
|
||||
在主题中,你可以通过 `finder` 名称和方法名及对应的参数来获取数据,例如:
|
||||
|
||||
```html
|
||||
<div th:text="${myPluginLinkFinder.listAll()}">
|
||||
</div>
|
||||
```
|
||||
|
||||
模板语法参考:[Thymeleaf](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)。
|
@@ -0,0 +1,365 @@
|
||||
---
|
||||
title: 编写控制器
|
||||
description: 了解如何为自定义模型编写控制器
|
||||
---
|
||||
|
||||
控制器是 Halo 的关键组件,它们负责对每个自定义模型对象进行操作,协调所需状态和当前状态,参考: [控制器概述](../../basics/framework.md#controller)。
|
||||
|
||||
控制器通常在具有一般事件序列的控制循环中运行:
|
||||
|
||||
1. 观察:每个控制器将被设计为观察一组自定义模型对象,例如文章的控制器会观察文章对象,插件的控制器会观察插件自定义模型对象等。
|
||||
2. 比较:控制器将对象配置的期望状态与其当前状态进行比较,以确定是否需要更改,例如插件的 `spec.enabled` 为 `true`,而插件的当前状态是未启动,则插件控制器会处理启动插件的逻辑。
|
||||
3. 操作:控制器将根据比较的结果执行相应的操作,以确保对象的实际状态与其期望状态一致,例如插件期望启动,插件控制器会处理启动插件的逻辑。
|
||||
3. 重复:上述所有步骤都由控制器重复执行直到与期望状态一致。
|
||||
|
||||
这是一个描述控制器作用的例子:房间里的温度自动调节器。
|
||||
|
||||
当你设置了温度,告诉了温度自动调节器你的期望状态(Desired State)。
|
||||
房间的实际温度是当前状态(Current State)。 通过对设备的开关控制,温度自动调节器让其当前状态接近期望状态,未到达期望状态则继续调节,直到达到期望状态。
|
||||
|
||||
在 Halo 中控制器的运行部分已经有一个默认实现,你只需要编写控制器的调谐的逻辑也就是 [控制器概述](../../basics/framework.md#controller) 中的所说的 Reconciler 即可。
|
||||
|
||||
## 编写 Reconciler
|
||||
|
||||
Reconciler 是控制器的核心,它是一个接口,你需要实现它的 `reconcile()` 方法,该方法接收一个 `Reconciler.Request` 对象,它包含了当前自定义模型对象的名称,你可以通过它来获取自定义模型对象的当前状态和期望状态,然后编写调谐的逻辑。
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new Post())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以上是一个简单的 Reconciler 实现,它实现了 `reconcile()` 方法,然后在 `setupWith()` 方法中将其通过 `ControllerBuilder` 构建为一个控制器并指定了
|
||||
它要观察的自定义模型对象为`Post`,当文章自定义模型对象发生变化时,`reconcile()` 方法就会被调用,从 `Request request` 参数中你可以获得当前发生变化的文章自定义模型对象的名称,然后你就可以通过名称来查询到自定义模型对象进行调谐了。
|
||||
|
||||
### 构建控制器
|
||||
|
||||
`setupWith()` 方法用于根据当前类的 `reconcile` 方法构建控制器,你可以通过 `ControllerBuilder` 提供的方法来构建并定制控制器:
|
||||
|
||||
```java
|
||||
public class ControllerBuilder {
|
||||
private final String name;
|
||||
private Duration minDelay;
|
||||
private Duration maxDelay;
|
||||
private final Reconciler<Reconciler.Request> reconciler;
|
||||
private Supplier<Instant> nowSupplier;
|
||||
private Extension extension;
|
||||
private ExtensionMatcher onAddMatcher;
|
||||
private ExtensionMatcher onDeleteMatcher;
|
||||
private ExtensionMatcher onUpdateMatcher;
|
||||
private ListOptions syncAllListOptions;
|
||||
private boolean syncAllOnStart = true;
|
||||
private int workerCount = 1;
|
||||
}
|
||||
```
|
||||
|
||||
- `name`:控制器的名称,用于标识控制器。
|
||||
- `minDelay`:控制器的最小延迟,用于控制控制器的最小调谐间隔,默认为 5 毫秒。
|
||||
- `maxDelay`:控制器的最大延迟,用于控制控制器的最大调谐间隔,默认为 1000 秒。
|
||||
- `reconciler`:控制器的调谐器,用于执行调谐逻辑,你需要实现 `Reconciler` 接口。
|
||||
- `nowSupplier`:用于获取当前时间的供应商,用于控制器的时间戳,默认使用 `Instant.now()` 获取当前时间。
|
||||
- `extension`:控制器要观察的自定义模型对象。
|
||||
- `onAddMatcher`:用于匹配添加事件的匹配器,当自定义模型对象被创建时会触发。
|
||||
- `onDeleteMatcher`:用于匹配删除事件的匹配器,当自定义模型对象被删除时会触发。
|
||||
- `onUpdateMatcher`:用于匹配更新事件的匹配器,当自定义模型对象被更新时会触发。
|
||||
- `syncAllListOptions`:用于同步所有自定义模型对象的查询条件,仅当 `syncAllOnStart` 为 `true` 时生效。
|
||||
- `syncAllOnStart`:是否在控制器启动时同步所有自定义模型对象,默认为 `true`,可以配合 `syncAllListOptions` 使用以缩小需要同步的对象范围避免不必要的同步,例如只同步某个用户创建的文章或者某个固定名称的 ConfigMap 对象。如果你的控制器不需要同步所有对象,可以将其设置为 `false`。
|
||||
- `workerCount`:控制器的工作线程数,用于控制控制器的并发度,如果你的控制器需要处理大量的对象,可以将其设置为大于 1 的值,以提高控制器的处理能力,但需要注意的是并发度越高,系统的负载也会越高。这里的并发度是指控制器的并发度,但是每个控制器还是单线程执行的。
|
||||
|
||||
#### ExtensionMatcher
|
||||
|
||||
`onAddMatcher/onUpdateMatcher/onDeleteMatcher` 都是 `ExtensionMatcher` 类型,用于决定当自定义模型对象发生变化时是否触发控制器:
|
||||
|
||||
```java
|
||||
public interface ExtensionMatcher {
|
||||
boolean match(Extension extension);
|
||||
}
|
||||
```
|
||||
|
||||
这里`match` 方法的 `Extension` 参数类型与 `ControllerBuilder` 中的 `extension` 类型始终是一致的,因此可以直接通过强制类型转换来得到需要的类型。
|
||||
|
||||
比如我们想要观察文章对象,但是只想观察文章对象中 `visible` 字段为 `PUBLIC` 的文章,可以这样
|
||||
|
||||
```java
|
||||
public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
// 只想观察 VisibleEnum.PUBLIC 的文章
|
||||
ExtensionMatcher extensionMatcher = extension -> {
|
||||
var post = (Post) extension;
|
||||
return VisibleEnum.PUBLIC.equals(post.getSpec().getVisible());
|
||||
};
|
||||
return builder
|
||||
.extension(new Post())
|
||||
.onAddMatcher(extensionMatcher)
|
||||
.onUpdateMatcher(extensionMatcher)
|
||||
.onDeleteMatcher(extensionMatcher)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 控制启动时同步的范围
|
||||
|
||||
如果想要在控制器启动时控制同步对象的范围,可以通过 `syncAllListOptions` 和 `syncAllOnStart` 来实现,例如只同步某个用户创建的文章:
|
||||
|
||||
```java
|
||||
public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new Post())
|
||||
.syncAllListOptions(ListOptions.builder()
|
||||
.fieldQuery(QueryFactory.equal("spec.owner", "guqing"))
|
||||
.build()
|
||||
)
|
||||
.syncAllOnStart(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reconciler 的返回值
|
||||
|
||||
`reconcile()` 方法的返回值是一个 `Result` 对象,它包含了调谐的结果,你可以通过它来告诉控制器是否需要重试,如果需要重试则控制器会在稍后再次调用 `reconcile()` 方法,而这个过程会一直重复,直到 `reconcile()` 方法返回成功为止,这个过程被称之为调谐循环(Reconciliation Loop)。
|
||||
|
||||
```java
|
||||
record Result(boolean reEnqueue, Duration retryAfter) {}
|
||||
```
|
||||
|
||||
`Result` 对象包含了两个属性:reEnqueue 和 retryAfter,reEnqueue 用于标识是否需要重试,retryAfter 用于标识重试的时间间隔,如果 reEnqueue 为 true 则会在 retryAfter 指定的时间间隔后再次调用 `reconcile()` 方法,如果 reEnqueue 为 false 则不会再次调用 `reconcile()` 方法。
|
||||
|
||||
在没有特殊需要时,`retryAfter` 可以不指定,控制器会有一套默认的重试策略。
|
||||
|
||||
如果直接返回 `null` 则会被视为成功,效果等同于返回 `new Result(false, null)`。
|
||||
|
||||
### Reconciler 的异常处理
|
||||
|
||||
当 `reconcile()` 方法抛出异常时,控制器会将异常记录到日志中,然后会将 `Request request` 对象重新放入队列中,等待下次调用 `reconcile()` 方法,这个过程会一直重复,直到 `reconcile()` 成功,对于默认重试策略,每次重试间隔会越来越长,直到达到最长间隔后不再增加。
|
||||
|
||||
## 控制器示例
|
||||
|
||||
本章节将通过一个简单的示例来演示如何编写控制器。
|
||||
|
||||
### 场景:事件管理系统
|
||||
|
||||
创建一个名为 ”EventTracker“ 的自定义模型,用于管理和追踪组织内的各种事件。这些事件可以是会议、研讨会、社交聚会或任何其他类型的组织活动。
|
||||
“EventTracker“ 自定义模型将提供一个框架,用于记录事件的详细信息,如时间、地点、参与者和状态。
|
||||
|
||||
由于这里的重点是控制器,因此我们将忽略自定义模型的详细信息,只关注控制器的实现,一个可能的 “EventTracker” 数据结构如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: tracker.halo.run/v1alpha1
|
||||
kind: EventTracker
|
||||
metadata:
|
||||
name: event-tracker-1
|
||||
spec:
|
||||
eventName: "Halo Meetup"
|
||||
eventDate: "2024-01-20T12:00:00Z"
|
||||
location: "Chengdu"
|
||||
participants: ["@sig-doc", "@sig-console", "@sig-halo"]
|
||||
description: "Halo Meetup in Chengdu"
|
||||
status:
|
||||
phase: "Planned" # Planned, Ongoing, Completed
|
||||
participants: []
|
||||
conditions:
|
||||
- type: "Invalid"
|
||||
status: "True"
|
||||
reason: "InvalidEventDate"
|
||||
message: "Event date is invalid"
|
||||
```
|
||||
|
||||
业务逻辑处理:
|
||||
|
||||
1. 事件创建:
|
||||
|
||||
- 当新的 EventTracker 资源被创建时,控制器需验证所有必要字段的存在和格式正确性。
|
||||
- 初始化事件状态为 Planned。
|
||||
|
||||
2. 事件更新:
|
||||
|
||||
- 检查 eventDate、location 和 participants 字段的变更。
|
||||
- 如果接近事件日期,自动更新状态为 Ongoing。
|
||||
|
||||
3. 状态管理:
|
||||
|
||||
- 根据当前日期和事件日期自动管理 phase 字段。
|
||||
- 当事件日期过去时,将状态更新为 Completed。
|
||||
4. 数据验证和完整性:
|
||||
- 确保所有输入数据的格式正确且合理。
|
||||
- 如有不一致或缺失的重要信息,记录警告或错误。
|
||||
5. 事件提醒和通知:
|
||||
- 在事件状态改变或临近事件日期时发送通知。
|
||||
6. 清理和维护:
|
||||
- 对于已完成的事件,提供自动清理机制,例如在事件结束后一定时间内删除资源。
|
||||
|
||||
首先实现 EventTracker 控制器的协调循环主体,通过依赖注入 `ExtensionClient` 可以用于获取当前变更的对象:
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EventTrackerReconciler implements Reconciler<Reconciler.Request> {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new EventTracker())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后在 `reconcile()` 方法中根据 `EventTracker` 对象的状态来执行响应的操作,确保执行逻辑是是幂等的,这意味着即使多次执行相同操作,结果也应该是一致的。
|
||||
|
||||
```java
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> {
|
||||
// 获取到当前变更的 EventTracker 对象
|
||||
// 1. 检查必要字段的存在和格式正确性
|
||||
// 2. 初始化事件状态为 Planned
|
||||
if (eventTracker.getStatus() == null) {
|
||||
eventTracker.setStatus(new EventTracker.Status());
|
||||
}
|
||||
var status = eventTracker.getStatus();
|
||||
if (status.getPhase() == null) {
|
||||
status.setPhase(EventTracker.Phase.PLANNED);
|
||||
}
|
||||
|
||||
var eventName = eventTracker.getSpec().getEventName();
|
||||
if (StringUtils.isBlank(eventName)) {
|
||||
Condition condition = Condition.builder()
|
||||
.type("Invalid")
|
||||
.reason("InvalidEventName")
|
||||
.message("Event name is invalid")
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
status.getConditions().addAndEvictFIFO(condition);
|
||||
}
|
||||
|
||||
client.update(eventTracker);
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
```
|
||||
|
||||
上述,我们通过 `client.fetch()` 方法获取到了当前变更的 `EventTracker` 对象,然后根据 `EventTracker` 对象的状态来执行响应的操作,例如初始化事件状态为 Planned,检查必要字段的存在和格式正确性等,但需要注意控制器的执行是异步的,如果我们通过 `EventTracker` 的 API 来创建或更改了一个 `EventTracker` 对象,那么 API 会在控制器执行之前返回结果,这意味着在用户界面看到的结果可能不是最新的,并且可能会在稍后更新。
|
||||
|
||||
对于上述校验 `eventName` 的逻辑只是保证后续的执行是可靠的,如果有些字段是必须的,那么我们可以通过 `@Schema` 注解来标注,为了让控制器中校验字段失败的信息能够呈现到用户界面,我们通过向 `status.conditions` 中添加了一条 Condition 记录来用于记录这个事件,再用户界面可以展示这个 Condition 记录的信息以让用户知晓。
|
||||
|
||||
最后,我们通过 `client.update()` 方法来更新 `EventTracker` 对象,这个过程就是将实际状态回写到 `EventTracker` 对象并应用到数据库中,这样就完成了一次调谐。
|
||||
|
||||
当 `EventTracker` 对象发生变更时,控制器也会被执行,这时我们可以根据 `EventTracker` 对象的状态来执行响应的操作,例如检查和更新 `eventDate`、`location` 和 `participants` 字段的变更,如果接近事件日期,自动更新状态为 Ongoing。
|
||||
|
||||
```java
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> {
|
||||
// ...此处省略之前的逻辑
|
||||
if (isApproach(eventTracker.getSpec().getEventDate())) {
|
||||
status.setPhase(EventTracker.Phase.ONGOING);
|
||||
sendNotification(eventTracker, "Event is ongoing");
|
||||
}
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
```
|
||||
|
||||
这里我们通过 `isApproach()` 方法来表示判断是否接近事件日期,如果接近则更新状态为 Ongoing,并使用 `sendNotification` 来发送发送通知。
|
||||
|
||||
> 为了简化示例,我们省略了 `isApproach()` 和 `sendNotification` 方法的实现。
|
||||
|
||||
还可以根据 `spec.participants` 字段来解析参与者信息,然后将其添加到 `status.participants` 中,这样就可以在用户界面看到参与者信息了。
|
||||
|
||||
```java
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> {
|
||||
// ...此处省略之前的逻辑
|
||||
var participants = eventTracker.getSpec().getParticipants();
|
||||
resolveParticipants(participants).forEach(status::addParticipant);
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 Finalizers
|
||||
|
||||
`Finalizers` 允许控制器实现异步预删除钩子。假设您为正在实现的 API 类型的每个对象创建了一个外部资源,例如存储桶,并且您希望在从 Halo 中删除相应对象
|
||||
时清理外部资源,您可以使用终结器来删除外部资源资源。
|
||||
|
||||
比如 `EventTracker` 对象被删除时,我们需要删除 `EventTracker` 对象记录的日志,这时我们可以通过 `Finalizers` 来实现。
|
||||
|
||||
首先我们需要在 `reconcile()` 的开头判断 `EventTracker` 对象的 `metadata.deletionTimestamp` 是否存在,如果存在则表示 `EventTracker` 对象被删除了,
|
||||
这时我们就可以执行清理操作。
|
||||
|
||||
```java
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> {
|
||||
if (ExtensionOperator.isDeleted(eventTracker)) { // 1. 判断是否被删除
|
||||
// 2. 调用 removeFinalizers 方法移除终结器(稍后会说明)
|
||||
ExtensionUtil.removeFinalizers(eventTracker.getMetadata(), Set.of(FINALIZER_NAME));
|
||||
// 3. 执行清理操作
|
||||
cleanUpLogsForTracker(eventTracker);
|
||||
// 4. 更新 EventTracker 对象将变更应用到数据库中
|
||||
client.update(eventTracker);
|
||||
// 5. return 避免执行后续逻辑
|
||||
return;
|
||||
}
|
||||
// ...此处省略之前的逻辑
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
```
|
||||
|
||||
1. `ExtensionOperator.isDeleted` 方法是 Halo 提供的工具方法,用于判断对象是否被删除,它会判断 `metadata.deletionTimestamp` 是否存在,如果存在则表示对象被标记删除了。
|
||||
关于自定义模型对象的删除可以参考:[自定义模型对象生命周期](../../basics/framework.md#extension-lifecycle)
|
||||
2. `ExtensionUtil.removeFinalizers` 方法是 Halo 提供的工具方法,用于移除对象的终结器,它接收两个参数,第一个参数是对象的元数据,第二个参数是要移除的终结器名称集合,它来自 `run.halo.app.extension.ExtensionUtil`。
|
||||
3. `cleanUpLogsForTracker` 方法是我们自己实现的,这里的示例是用于清理 `EventTracker` 对象记录的日志,你可以根据自己的业务需求来实现,如清理外部资源等。
|
||||
|
||||
经过上述步骤,我们只是写了移除终结器但是发现没有添加终结器的逻辑,添加终结器的逻辑需要在判断删除之后,`metadata.finalizers` 是一个字符串集合,用于标识对象是否可回收,如果 `metadata.finalizers` 不为空则表示对象不可回收,否则表示对象可回收,我们可以通过 `ExtensionUtil.addFinalizers` 方法来添加终结器。
|
||||
|
||||
最佳实践是,一个控制器最多添加一个终结器,名称为了防止冲突可以使用当前业务的 `group/终结器名称` 来命名,例如 `tracker.halo.run/finalizer`,例如在 Halo 中文章的控制器使用了一个终结器,但可能插件也会定义一个文章控制器来扩展文章的业务,那么根据最佳实践命名终结器可以避免冲突。
|
||||
|
||||
```java
|
||||
private static final String FINALIZER_NAME = "tracker.halo.run/finalizer";
|
||||
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> {
|
||||
if (ExtensionOperator.isDeleted(eventTracker)) {
|
||||
// ... 省略删除逻辑
|
||||
}
|
||||
// 添加终结器
|
||||
ExtensionUtil.addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));
|
||||
// ...此处省略之前的逻辑
|
||||
// 会在更新时将终结器的变更写入到数据库中
|
||||
client.update(eventTracker);
|
||||
});
|
||||
}
|
||||
```
|
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: 静态资源代理
|
||||
description: 了解如果使用静态资源代理来访问插件中的静态资源
|
||||
---
|
||||
|
||||
插件中的静态资源如图片等如果想被外部访问到,需要放到 `src/main/resources` 目录下,并通过创建 `ReverseProxy` 自定义模型对象来进行静态资源代理访问。
|
||||
|
||||
例如 `src/main/resources` 下的 `static` 目录下有一张 `halo.jpg`:
|
||||
|
||||
1. 首先需要在 `src/main/resources/extensions` 下创建一个 `yaml`,文件名可以任意。
|
||||
2. 声明 `ReverseProxy` 对象如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ReverseProxy
|
||||
metadata:
|
||||
# 为了避免与其他插件冲突,推荐带上插件名称前缀
|
||||
name: my-plugin-fake-reverse-proxy
|
||||
rules:
|
||||
- path: /res/**
|
||||
file:
|
||||
directory: static
|
||||
# 如果想代理 static 下所有静态资源则省略 filename 配置
|
||||
filename: halo.jpg
|
||||
```
|
||||
|
||||
插件启动后会根据 `/plugins/{plugin-name}/assets/**` 规则生成访问路径,
|
||||
因此该 `ReverseProxy` 的访问路径为: `/plugins/my-plugin/assets/res/halo.jpg`。
|
||||
|
||||
- `rules` 下可以添加多组规则。
|
||||
- `path` 为路径前缀。
|
||||
- `file` 表示访问文件系统,目前暂时仅支持这一种。
|
||||
- `directory` 表示要代理的目标文件目录,它相对于 `src/main/resources/` 目录。
|
||||
- `filename` 表示要代理的目标文件名。
|
||||
|
||||
`directory` 和 `filename` 都是可选的,但必须至少有一个被配置。
|
@@ -0,0 +1,307 @@
|
||||
---
|
||||
title: API 权限控制
|
||||
description: 了解如果对插件中的 API 定义角色模板以接入权限控制
|
||||
---
|
||||
|
||||
插件中的 APIs 无论是自定义模型自动生成的 APIs 或者是通过 `@Controller` 自定义的 APIs 都只有超级管理员能够访问,如果想将这些 APIs 授权给其他用户访问,
|
||||
则需要定义一些[角色模板](../../basics/framework.md#rbac)的资源以便可以在用户界面上将其分配给其他角色使用。
|
||||
|
||||
## 角色模板定义
|
||||
|
||||
定义角色模板需要遵循一定的规范:
|
||||
|
||||
- **文件位置和标记**:角色模板定义文件存放于 `src/main/resources/extensions`,文件名可以任意,它的 kind 为 Role 且必须具有标签 `halo.run/role-template: "true"` 来标识其为模板。
|
||||
- **角色类型**:通常,我们为同一种资源定义两种角色模板:只读权限和管理权限,分别对应 `view` 和 `manage`,如果需要更细粒度的控制,可以定义更多的角色模板。
|
||||
- **角色名称**:角色名称必须以插件名作为前缀,以避免与其他插件冲突,例如 `my-plugin-role-view-persons`。
|
||||
- **角色依赖**:如果一个角色需要依赖于另一个角色,可以通过 `rbac.authorization.halo.run/dependencies` 作为 key 的 `metadata.annotations` 来声明依赖关系。
|
||||
- **UI 权限**:如果需要在前端界面上控制某个角色的权限,可以通过 `rbac.authorization.halo.run/ui-permissions` 作为 key 的 `metadata.annotations` 来声明。
|
||||
- **角色模板分组**:如果需要将多个角色模板归为一组显示,可以通过 `rbac.authorization.halo.run/module` 作为 key 的 `metadata.annotations` 来声明分组名称。
|
||||
- **角色显示名称**:如果需要在前端界面上显示角色的友好名称,可以通过 `rbac.authorization.halo.run/display-name` 作为 key 的 `metadata.annotations` 来声明显示名称。
|
||||
- **隐藏角色模板**:如果不想在前端界面上显示某个角色模板,可以通过 `halo.run/hidden: "true"` 的 `metadata.labels` 来隐藏角色模板。
|
||||
|
||||
角色模板定义的基本框架如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-name
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
rules:
|
||||
- apiGroups: []
|
||||
resources: []
|
||||
resourceNames: []
|
||||
verbs: []
|
||||
- nonResourceURLs: []
|
||||
verbs: []
|
||||
```
|
||||
|
||||
在遵循上述规范的基础上,最重要的是定义 `rules` 字段,它是一个数组,用于定义角色模板的权限规则,规则分为两种类型:[资源型](#resource-rules)和[非资源型](#non-resource-rules)。
|
||||
|
||||
### 资源型规则 {#resource-rules}
|
||||
|
||||
资源型规则用于定义对资源的操作权限,API 符合以下特征:
|
||||
|
||||
- 以 `/api` 开头,且以 `/api/<version>/<resource>[/<resourceName>/<subresource>]` 规则组成 APIs,最少路径层级为 3 即 `/api/<version>/<resource>`,最多路径层级为 5 即包含 `<resourceName>` 和 `<subresource>`,例如 `/api/v1/posts`。
|
||||
- 以 `/apis/<group>/<version>/<resource>[/<resourceName>/<subresource>]` 规则组成的 APIs,最少路径层级为 4 即 `/apis/<group>/<version>/<resource>`,最多路径层级为 6 即包含 `<resourceName>` 和 `<subresource>`,例如 `/apis/my-plugin.halo.run/v1alpha1/persons`。
|
||||
|
||||
:::info 注
|
||||
`[]`包裹的部分表示可选,`/api` 前缀被 Halo 保留,不允许插件定义以 `/api` 开头的资源型 APIs,所以插件的资源型 APIs 都是以 `/apis` 开头的。
|
||||
:::
|
||||
|
||||
通常可以通过 `apiGroups`、`resources`、`resourceNames`、`verbs` 来组合定义。
|
||||
例如对于资源型 API `GET /apis/my-plugin.halo.run/v1alpha1/persons`,可以定义如下规则:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- apiGroups: [ "my-plugin.halo.run" ]
|
||||
resources: [ "my-plugin/persons" ]
|
||||
verbs: [ "list" ]
|
||||
```
|
||||
|
||||
而对于资源型 API `GET /apis/my-plugin.halo.run/v1alpha1/persons/zhangsan`,可以定义如下规则:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- apiGroups: [ "my-plugin.halo.run" ]
|
||||
resources: [ "my-plugin/persons" ]
|
||||
resourceNames: [ "zhangsan" ]
|
||||
verbs: [ "get" ]
|
||||
```
|
||||
|
||||
关于 `verbs` 的详细说明请参考 [Verbs 详解](#verbs)。
|
||||
|
||||
### 非资源型规则 {#non-resource-rules}
|
||||
|
||||
凡是不符合资源型 APIs 规则的 APIs 都被定型为非资源型 APIs,例如 `/healthz`,可以使用以下配置方式:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- nonResourceURLs: ["/healthz", "/healthz/*"]
|
||||
verbs: [ "get", "create"]
|
||||
```
|
||||
|
||||
非资源型规则使用 `nonResourceURLs` 来定义,其中 `nonResourceURLs` 是一个字符串数组,用于定义非资源型 APIs 的路径,`verbs` 用于定义非资源型 APIs 的请求动词。
|
||||
|
||||
`nonResourceURL` 中的 `*` 是一个全局通配符,表示匹配所有路径,如 `/healthz/*` 表示匹配 `/healthz/` 下的所有路径。
|
||||
|
||||
### 示例:定义人员管理角色模板
|
||||
|
||||
以下 YAML 文件展示了如何定义用于人员管理的角色模板:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
# 使用 plugin name 作为前缀防止与其他插件冲突,比如这里的 my-plugin
|
||||
name: my-plugin-role-view-persons
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Persons Management"
|
||||
rbac.authorization.halo.run/display-name: "Person Manage"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:my-plugin:person:view"]
|
||||
rules:
|
||||
- apiGroups: ["my-plugin.halo.run"]
|
||||
resources: ["my-plugin/persons"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: my-plugin-role-manage-persons
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
[ "role-template-view-person" ]
|
||||
rbac.authorization.halo.run/module: "Persons Management"
|
||||
rbac.authorization.halo.run/display-name: "Person Manage"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:my-plugin:person:manage"]
|
||||
rules:
|
||||
- apiGroups: [ "my-plugin.halo.run" ]
|
||||
resources: [ "my-plugin/persons" ]
|
||||
verbs: [ "get", "list" ]
|
||||
```
|
||||
|
||||
上述便是根据 [自定义模型](./extension.md) 章节中定义的 Person 自定义模型来配置角色模板的示例。
|
||||
|
||||
1. 定义了一个用于管理 Person 自定义模型对象的角色模板 `my-plugin-role-manage-persons`,它具有所有权限。
|
||||
2. 定义了一个只允许查询 Person 资源的角色模板 `my-plugin-role-view-persons`。
|
||||
3. `metadata.name` 的命名规则参考 [metadata name 命名规范](../server/extension.md#metadata-name)。
|
||||
|
||||
下面让我们回顾一下这些配置:
|
||||
|
||||
`rules` 是个数组,它允许配置多组规则:
|
||||
|
||||
- `apiGroups` 对应 `GVK` 中的 `group` 所声明的值。
|
||||
- `resources` 对应 API 中的 resource 部分。
|
||||
- `verbs` 表示请求动词,可选值为 "create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"。参考 [Verbs 详解](#verbs)。
|
||||
|
||||
`metadata.labels` 中必须包含 `halo.run/role-template: "true"` 以表示它此资源要作为角色模板。
|
||||
|
||||
`metadata.annotations` 中:
|
||||
|
||||
- `rbac.authorization.halo.run/dependencies`:用于声明角色间的依赖关系,例如管理角色必须要依赖查看角色,以避免分配了管理权限却没有查看权限的情况。
|
||||
- `rbac.authorization.halo.run/module`:角色模板分组名称。在此示例中,管理 Person 的模板角色将和查看 Person 的模板角色将被在 UI 层面归为一组展示。
|
||||
- `rbac.authorization.halo.run/display-name`:模板角色的显示名称,用于展示为用户可读的名称信息。
|
||||
|
||||
### UI 权限控制 {#ui-permissions}
|
||||
|
||||
通过在角色模板的 `metadata.annotations` 中定义 `rbac.authorization.halo.run/ui-permissions` 来控制 UI 权限,这样可以在前端界面通过这个权限来控制菜单或者页面按钮是否展示。
|
||||
|
||||
值的规则为 `plugin:{your-plugin-name}:scope-name`, `scope-name` 为你自定义的权限名称,如上面的示例中的 `plugin:my-plugin:person:view` 和 `plugin:my-plugin:person:manage`。
|
||||
|
||||
你可以在 UI 层面使用这个权限来控制菜单是否展示:
|
||||
|
||||
```javascript
|
||||
{
|
||||
path: "",
|
||||
name: "HelloWorld",
|
||||
component: DefaultView,
|
||||
meta: {
|
||||
permissions: ["plugin:my-plugin:person:view"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 该配置示例为在插件前端部分入口文件 `index.ts`。
|
||||
|
||||
或者在按钮或页面组件中使用这个权限来控制是否展示:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<!-- HasPermission 组件不需要导入,直接使用即可 -->
|
||||
<HasPermission :permissions="['plugin:my-plugin:person:view']">
|
||||
<UserFilterDropdown
|
||||
v-model="selectedUser"
|
||||
label="用户"
|
||||
/>
|
||||
</HasPermission>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Verbs 详解 {#verbs}
|
||||
|
||||
`verbs` 字段用于指定用户或服务在特定资源上能执行的操作类型。这些操作被定义为一组“动词”,每个动词与相应的 HTTP 请求方法相对应。为了更好地理解如何确定合适的 `verbs`,以下是详细的解释和每种动词的具体用途:
|
||||
|
||||
动词和对应的 HTTP 方法:
|
||||
|
||||
- create: 对应 HTTP 的 POST 方法。用于创建一个新的资源实例,如果是创建子资源且不需要资源名称可以使用 `-` 表示缺省,如 `POST /apis/my-plugin.halo.run/v1alpha1/persons/-/subresource`,同时需要注意 `POST /apis/my-plugin.halo.run/v1alpha1/persons/{some-name}` 不是一个符合规范的 create 操作,创建资源不应该包含资源名称。
|
||||
- get: 对应 HTTP 的 GET 方法。用于获取单个资源的详细信息,即 API 中包含 resourceName 部分如 `GET /apis/my-plugin.halo.run/v1alpha1/persons/zhangsan`。
|
||||
- list: 同样对应 HTTP 的 GET 方法,但用于获取资源的集合(列表),这通常涵盖了多个资源实例的摘要或详细信息,如 `GET /apis/my-plugin.halo.run/v1alpha1/persons`。
|
||||
- watch: 也是对应 HTTP 的 GET 方法。用于实时监控资源或资源集合的变化,通常是通过 WebSocket 连接来实现的,如 `ws://localhost:8090/apis/my-plugin.halo.run/v1alpha1/persons`。
|
||||
- update: 对应 HTTP 的 PUT 方法。用于更新现有资源的全部内容。
|
||||
- patch: 对应 HTTP 的 PATCH 方法。用于对现有资源进行部分更新。
|
||||
- delete: 对应 HTTP 的 DELETE 方法。用于删除单个资源, 即 API 中包含 resourceName 部分如 `DELETE /apis/my-plugin.halo.run/v1alpha1/persons/zhangsan`。
|
||||
- deletecollection: 同样对应 HTTP 的 DELETE 方法,但用于删除一个资源集合。
|
||||
|
||||
可以使用如下表格来简化理解:
|
||||
|
||||
| Verb | HTTP Method(s) | Description |
|
||||
|--------------------|----------------|--------------------------|
|
||||
| `create` | POST | 创建新资源实例 |
|
||||
| `get` | GET | 获取单个资源详细信息 |
|
||||
| `list` | GET | 获取资源列表 |
|
||||
| `watch` | GET | 监控资源或资源集合的变化 |
|
||||
| `update` | PUT | 更新现有资源 |
|
||||
| `patch` | PATCH | 部分更新资源 |
|
||||
| `delete` | DELETE | 删除单个资源 |
|
||||
| `deletecollection` | DELETE | 删除资源集合 |
|
||||
|
||||
## 默认角色
|
||||
|
||||
在 Halo 中,每个访问者都至少有一个角色,包括未登录的用户(被称为匿名用户)它们会拥有角色为 `anonymous` 的角色,而已登录的用户则会至少拥有一个角色名为 `authenticated` 的角色,
|
||||
但这两个角色不会显示在角色列表中。
|
||||
|
||||
`anonymous` 角色的定义参考 [anonymous 角色](https://github.com/halo-dev/halo/blob/main/application/src/main/resources/extensions/role-template-anonymous.yaml)。
|
||||
|
||||
`authenticated` 角色的定义参考 [authenticated 角色](https://github.com/halo-dev/halo/blob/main/application/src/main/resources/extensions/role-template-authenticated.yaml)。
|
||||
|
||||
进入角色列表页面,你会看到一些内置角色,用于方便你快速的分配权限给用户,并可以基于这些角色来创建新的角色:
|
||||
|
||||
- 超级管理员:拥有所有权限,不可删除,不可编辑。
|
||||
- 访客:拥有默认的 `anonymous` 和 `authenticated` 角色的权限。
|
||||
- 投稿者:拥有“允许投稿”的权限。
|
||||
- 作者:拥有“允许管理自己的文章”和”允许发布自己的文章“的权限。
|
||||
- 文章管理员:拥有“允许管理所有文章”的权限。
|
||||
|
||||
## 角色绑定
|
||||
|
||||
角色绑定用于将角色中定义的权限授予一个或一组用户。它包含主体列表(用户)以及对所授予角色的引用。
|
||||
|
||||
角色绑定示例:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
# 这个角色绑定允许 "guqing" 用户拥有 "post-reader" 角色的权限
|
||||
# 你需要在 Halo 中已经定义了一个名为 "post-reader" 的角色。
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: guqing-post-reader-binding
|
||||
roleRef:
|
||||
# "roleRef" 指定了绑定到的角色
|
||||
apiGroup: ''
|
||||
# 这里必须是 Role
|
||||
kind: Role
|
||||
# 这里的 name 必须匹配到一个已经定义的角色
|
||||
name: post-reader
|
||||
subjects:
|
||||
- apiGroup: ''
|
||||
kind: User
|
||||
# 这里的 name 是用户的 username
|
||||
name: guqing
|
||||
```
|
||||
|
||||
在 Halo 中,当你给一个用户分配角色后,实际上就是创建了一个 ”RoleBinding” 对象来完成的。
|
||||
|
||||
## 聚合角色
|
||||
|
||||
你可以聚合角色来将多个角色的权限聚合到一个已有的角色中,这样你就不需要再为每个用户分配多个角色了。
|
||||
|
||||
聚合角色是通过在你定义的角色模板中添加 `"rbac.authorization.halo.run/aggregate-to-` 开头的 label 来实现的,例如
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
metadata:
|
||||
name: role-template-view-categories
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
rbac.authorization.halo.run/aggregate-to-editor: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "system:categories:view", "uc:categories:view" ]
|
||||
rules:
|
||||
- apiGroups: [ "content.halo.run" ]
|
||||
resources: [ "categories" ]
|
||||
verbs: [ "get", "list" ]
|
||||
```
|
||||
|
||||
`rbac.authorization.halo.run/aggregate-to-editor` 表示将 `role-template-view-categories` 角色聚合到 `editor` 角色中,这样所有拥有 `editor` 角色的用户都会拥有 `role-template-view-categories` 角色的权限。
|
||||
|
||||
如果你想将你写的资源型 APIs 公开给所有用户访问,这时你可以通过聚合角色来将你的资源型 APIs 的角色聚合到 `anonymous` 角色中,这样所有用户都可以访问你的资源型 APIs 了。
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: my-plugin-role-view-persons
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
rbac.authorization.halo.run/aggregate-to-anonymous: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Persons Management"
|
||||
rbac.authorization.halo.run/display-name: "Person Manage"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
["plugin:my-plugin:person:view"]
|
||||
rules:
|
||||
- apiGroups: ["my-plugin.halo.run"]
|
||||
resources: ["my-plugin/persons"]
|
||||
verbs: ["*"]
|
||||
```
|
||||
|
||||
`rbac.authorization.halo.run/aggregate-to-anonymous` 的写法就表示将 `my-plugin-role-view-persons` 角色聚合到 `anonymous` 角色中。
|
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: 获取插件配置
|
||||
description: 了解如何获取插件定义的设置表单对应的配置数据,以及如何在插件中使用配置数据。
|
||||
---
|
||||
|
||||
插件的 `plugin.yaml` 中允许配置 `settingName` 和 `configMapName` 字段,用于定义插件的个性化设置。
|
||||
本文介绍如何获取插件定义的设置表单对应的配置数据,以及如何在插件中使用配置数据。
|
||||
|
||||
## 概述
|
||||
|
||||
Halo 提供了两个 Bean 用于获取插件配置数据:`SettingFetcher` 和 `ReactiveSettingFetcher`,分别用于同步和异步获取配置数据。
|
||||
|
||||
以 `ReactiveSettingFetcher` 为例,提供了以下方法:
|
||||
|
||||
```java
|
||||
public interface ReactiveSettingFetcher {
|
||||
|
||||
<T> Mono<T> fetch(String group, Class<T> clazz);
|
||||
|
||||
@NonNull
|
||||
Mono<JsonNode> get(String group);
|
||||
|
||||
@NonNull
|
||||
Mono<Map<String, JsonNode>> getValues();
|
||||
}
|
||||
```
|
||||
|
||||
- `fetch` 方法用于获取指定分组的配置数据,并将其转换为指定的 Java 类型。
|
||||
- `get` 方法用于获取指定分组的配置数据,返回 `JsonNode` 类型。
|
||||
- `getValues` 方法用于获取所有配置数据,返回 `Map<String, JsonNode>` 类型,其中键为分组名称,值为配置对象。
|
||||
|
||||
`ReactiveSettingFetcher` 和 `SettingFetcher` 底层都对配置数据进行了缓存,以提高性能,并且在配置变更时会自动刷新缓存,所以直接调用这些方法即可获取最新的配置数据。
|
||||
|
||||
## 监听配置变更
|
||||
|
||||
当用户修改插件配置时,可以通过监听 `PluginConfigUpdatedEvent` 事件,执行相应的操作。`PluginConfigUpdatedEvent` 包含了配置变更前后的数据,使插件能够对变化做出响应。
|
||||
|
||||
```java
|
||||
public class PluginConfigUpdatedEvent extends ApplicationEvent {
|
||||
private final Map<String, JsonNode> oldConfig;
|
||||
private final Map<String, JsonNode> newConfig;
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 定义设置表单
|
||||
|
||||
假设插件定义了一个名为 `setting-seo` 的设置表单,其中包含了 `blockSpiders`、`keywords` 和 `description` 三个字段:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: Setting
|
||||
metadata:
|
||||
name: setting-seo
|
||||
spec:
|
||||
forms:
|
||||
- group: seo
|
||||
label: SEO 设置
|
||||
formSchema:
|
||||
- $formkit: checkbox
|
||||
name: blockSpiders
|
||||
label: "屏蔽搜索引擎"
|
||||
value: false
|
||||
- $formkit: textarea
|
||||
name: keywords
|
||||
label: "站点关键词"
|
||||
- $formkit: textarea
|
||||
name: description
|
||||
label: "站点描述"
|
||||
```
|
||||
|
||||
### 配置 plugin.yaml
|
||||
|
||||
在 `plugin.yaml` 中配置 `settingName` 和 `configMapName` 字段:
|
||||
|
||||
```yaml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: fake-plugin
|
||||
spec:
|
||||
displayName: "Fake Plugin"
|
||||
# ...
|
||||
configMapName: setting-seo-configmap
|
||||
settingName: setting-seo
|
||||
```
|
||||
|
||||
### 定义值类
|
||||
|
||||
为了方便使用,定义一个值类存储配置数据:
|
||||
|
||||
```java
|
||||
public record SeoSetting(boolean blockSpiders, String keywords, String description) {
|
||||
public static final String GROUP = "seo";
|
||||
}
|
||||
```
|
||||
|
||||
### 获取配置数据
|
||||
|
||||
通过依赖注入 `ReactiveSettingFetcher` 并使用 `fetch(group, type)` 方法查询配置:
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SeoService {
|
||||
private final ReactiveSettingFetcher settingFetcher;
|
||||
|
||||
public Mono<Void> checkSeo() {
|
||||
return settingFetcher.fetch(SeoSetting.GROUP, SeoSetting.class)
|
||||
.doOnNext(seoSetting -> {
|
||||
if (seoSetting.blockSpiders()) {
|
||||
// do something
|
||||
}
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监听配置变更
|
||||
|
||||
通过监听 `PluginConfigUpdatedEvent` 事件来处理配置变更:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class SeoConfigListener {
|
||||
@EventListener
|
||||
public void onConfigUpdated(PluginConfigUpdatedEvent event) {
|
||||
if (event.getNewConfig().containsKey(SeoSetting.GROUP)) {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过以上示例,可以看到如何使用 `ReactiveSettingFetcher` 获取配置数据,并通过监听 `PluginConfigUpdatedEvent` 来处理配置变更事件,确保系统能及时响应配置的变化。
|
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: 在插件中提供主题模板
|
||||
description: 了解如何为主题扩充模板。
|
||||
---
|
||||
|
||||
当你在插件中创建了自己的自定义模型后,你可能需要在主题端提供一个模板来展示这些数据,这一般有两种方式:
|
||||
|
||||
1. 插件规定模板名称,由主题选择性适配,如瞬间插件提供了 `/moments` 的路由渲染 `moment.html` 模板,主题可以选择性的提供 `moment.html` 模板来展示瞬间数据。
|
||||
2. 插件提供默认模板,当主题没有提供对应的模板时,使用默认模板,主题提供了对应的模板时,使用主题提供的模板。
|
||||
|
||||
## 创建一个模板
|
||||
|
||||
首先,你需要在插件的 `resources` 目录下创建一个 `templates` 目录,然后在 `templates` 目录下提供你的模板,例如:
|
||||
|
||||
```text
|
||||
├── templates
|
||||
│ ├── moment.html
|
||||
```
|
||||
|
||||
然后提供一个路由用于渲染这个模板,例如:
|
||||
|
||||
```java
|
||||
import run.halo.app.theme.TemplateNameResolver;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class MomentRouter {
|
||||
private final TemplateNameResolver templateNameResolver;
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> momentRouterFunction() {
|
||||
return route(GET("/moments"), this::renderMomentPage).build();
|
||||
}
|
||||
|
||||
Mono<ServerResponse> renderMomentPage(ServerRequest request) {
|
||||
// 或许你需要准备你需要提供给模板的默认数据,非必须
|
||||
var model = new HashMap<String, Object>();
|
||||
model.put("moments", List.of());
|
||||
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "moments")
|
||||
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 `TemplateNameResolver` 来解析模板名称,如果主题提供了对应的模板,那么就使用主题提供的模板,否则使用插件提供的模板,如果直接返回模板名称,那么只会使用主题提供的模板,如果主题没有提供对应的模板,那么会抛出异常。
|
||||
|
||||
## 模板片段
|
||||
|
||||
如果你的默认模板不止一个,你可能需要通过模板片段来抽取一些公共的部分,例如,你的插件提供了一个 `moment.html` 模板,你可能需要抽取一些公共的部分,例如头部、尾部等,你可以这样做:
|
||||
|
||||
```text
|
||||
├── templates
|
||||
│ ├── moment.html
|
||||
│ ├── fragments
|
||||
│ │ ├── layout.html
|
||||
```
|
||||
|
||||
然后定义一个 `layout.html` 模板,例如:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html th:fragment="layoutHtml(content)">
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title th:text="${title}">Moment</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<th:block th:replace="${content}" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
那么使用 `layout.html` 模板中提供的 `fragment` 时,你需要这样做:
|
||||
|
||||
```html
|
||||
<div th:replace="~{plugin:plugin-moment:fragments/layout :: layoutHtml(content = ~{::content})}">
|
||||
<th:block th:fragment="content"> Hello World </th:block>
|
||||
</div>
|
||||
```
|
||||
|
||||
`plugin:plugin-moment:fragments/layout` 即为使用 `layout.html` 模板的路径,必须以 `plugin:<your-plugin-name>:`前缀作为开头,`fragments/layout` 为模板相对于 `resources/templates` 的路径,`<your-plugin-name>` 即为你的插件名称。
|
||||
|
||||
**总结:**
|
||||
|
||||
1. 定义模板片段时与主题端定义模板片段时一样
|
||||
2. 使用模板片段时,必须以 `plugin:<your-plugin-name>:` 前缀作为开头,后跟模板相对于 `resources/templates` 的路径,例如 `plugin:plugin-moment:fragments/layout`,`plugin-moment` 即为你的插件名称,`fragments/layout` 为模板相对于 `resources/templates` 的路径。
|
||||
|
||||
参考:[Thymeleaf 模板片段](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#including-template-fragments)
|
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: 实现 WebSocket
|
||||
description: 了解在插件中如何实现 WebSocket。
|
||||
---
|
||||
|
||||
从 Halo 2.15.0 版本开始,核心提供了 WebSocketEndpoint 接口,其主要目的是为了方便插件实现 WebSocket 功能。
|
||||
|
||||
插件只需要实现这个接口,并添加 `@Component` 注解,WebSocket 实现将会在插件启动后生效,插件卸载后,该实现也会随之删除。
|
||||
|
||||
在插件中实现 WebSocket 的代码样例如下:
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MyWebSocketEndpoint implements WebSocketEndpoint {
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseApiVersion("my-plugin.halowrite.com/v1alpha1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String urlPath() {
|
||||
return "/resources";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketHandler handler() {
|
||||
return session -> {
|
||||
var messages = session.receive()
|
||||
.map(message -> {
|
||||
var payload = message.getPayloadAsText();
|
||||
return session.textMessage(payload.toUpperCase());
|
||||
});
|
||||
return session.send(messages);
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当插件安装成功后,可以通过路径 `/apis/my-plugin.halowrite.com/v1alpha1/resources` 访问。 示例如下:
|
||||
|
||||
```bash
|
||||
websocat --basic-auth admin:admin ws://127.0.0.1:8090/apis/my-plugin.halowrite.com/v1alpha1/resources
|
||||
```
|
||||
|
||||
需要注意的是, 插件中实现的 WebSocket 相关的 API 仍然受当前权限系统约束。
|
Reference in New Issue
Block a user