feat: 初始化 Halo 暗色模式插件
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试 - Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面 - 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等) - 设计文档和调查文档 - Halo 插件/主题开发 Agent Skills Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Reconciler (Controller Pattern)
|
||||
|
||||
A Kubernetes-style controller that watches Extension resources and continuously reconciles their desired state. More commonly used than `Watcher` in production plugins.
|
||||
|
||||
> Source: [Reconciler](https://github.com/halo-dev/halo/blob/main/api/src/main/java/run/halo/app/extension/controller/Reconciler.java) | [ControllerBuilder](https://github.com/halo-dev/halo/blob/main/api/src/main/java/run/halo/app/extension/controller/ControllerBuilder.java) | [Controller](https://github.com/halo-dev/halo/blob/main/api/src/main/java/run/halo/app/extension/controller/Controller.java)
|
||||
|
||||
## Basic Pattern
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MyReconciler implements Reconciler<Reconciler.Request> {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
public MyReconciler(ExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
// Fetch the extension being reconciled
|
||||
var myExt = client.fetch(MyExtension.class, request.name());
|
||||
if (myExt.isEmpty()) {
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
// Perform reconciliation logic
|
||||
var ext = myExt.get();
|
||||
// ... update annotations, labels, related resources, etc.
|
||||
|
||||
// Update the extension if modified
|
||||
client.update(ext);
|
||||
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new MyExtension())
|
||||
.syncAllOnStart(false)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ControllerBuilder Options
|
||||
|
||||
| Method | Description |
|
||||
| ---------------------------------- | ----------------------------------------------------------- |
|
||||
| `.extension(new MyExtension())` | The extension type to watch (required) |
|
||||
| `.syncAllOnStart(true)` | Reconcile all existing instances on startup (default: true) |
|
||||
| `.syncAllListOptions(listOptions)` | Filter which existing instances to sync on start |
|
||||
| `.minDelay(Duration)` | Minimum retry delay (default: 5ms) |
|
||||
| `.maxDelay(Duration)` | Maximum retry delay (default: 1000s) |
|
||||
| `.workerCount(int)` | Number of concurrent workers (default: 1) |
|
||||
| `.onAddMatcher(matcher)` | Filter which add events to process |
|
||||
| `.onUpdateMatcher(matcher)` | Filter which update events to process |
|
||||
| `.onDeleteMatcher(matcher)` | Filter which delete events to process |
|
||||
|
||||
## Result Types
|
||||
|
||||
```java
|
||||
// Success, do not retry
|
||||
return Result.doNotRetry();
|
||||
|
||||
// Requeue after a delay (for async operations or retry)
|
||||
return Result.requeue(Duration.ofSeconds(30));
|
||||
```
|
||||
|
||||
## Lifecycle Integration
|
||||
|
||||
Reconcilers are auto-discovered by Spring. If you need to start/stop manually (e.g., conditional on plugin config):
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MyReconciler implements Reconciler<Request>, SmartLifecycle {
|
||||
private Controller controller;
|
||||
private boolean running = false;
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
this.controller = builder.extension(new MyExtension()).build();
|
||||
return controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (controller != null && !running) {
|
||||
controller.start();
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (controller != null && running) {
|
||||
controller.dispose(); // or controller.stop()
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() { return running; }
|
||||
}
|
||||
```
|
||||
|
||||
## Finalizers
|
||||
|
||||
Use finalizers for cleanup before an extension is deleted:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
var ext = client.fetch(MyExtension.class, request.name()).orElse(null);
|
||||
if (ext == null) return Result.doNotRetry();
|
||||
|
||||
// Check if being deleted
|
||||
if (ext.getMetadata().getDeletionTimestamp() != null) {
|
||||
// Perform cleanup
|
||||
doCleanup(ext);
|
||||
// Remove finalizer to allow deletion
|
||||
ExtensionUtil.removeFinalizers(ext.getMetadata(), Set.of("my-plugin/finalizer"));
|
||||
client.update(ext);
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
// Add finalizer if not present
|
||||
ExtensionUtil.addFinalizers(ext.getMetadata(), Set.of("my-plugin/finalizer"));
|
||||
client.update(ext);
|
||||
|
||||
// Normal reconciliation
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Reconciler vs Watcher
|
||||
|
||||
| | Reconciler | Watcher |
|
||||
| --------------- | ---------------------------------------------------- | ----------------------- |
|
||||
| **Use case** | Continuous state reconciliation, finalizers, retries | One-time event handling |
|
||||
| **Persistence** | Queued, survives restarts | In-memory only |
|
||||
| **Concurrency** | Configurable worker count | Single threaded |
|
||||
| **Retry** | Built-in exponential backoff | No retry |
|
||||
Reference in New Issue
Block a user