# Rasepi 架构内部：插件、动作保护和管道

> 通过代码库中的真实代码，深入浅出地介绍 Rasepi 插件系统、动作保护管道和块级翻译引擎的实际工作原理。

2026-03-06 · Tim Cadenbach · https://www.tcdev.de/zh/blog/how-plugin-guardrail-and-pipeline-systems-work/

---

大多数文档平台谈论 "可扩展性 "就像航空公司谈论 "腿部空间 "一样。技术上存在，实际上却令人失望。我希望 Rasepi 的架构既具有真正的可扩展性，又不会变得不可预测，因此我们建立了三个环环相扣的系统：**插件**负责功能，**动作保护**负责控制，**管道**负责确定性执行。

本篇文章将介绍每个系统在我们的实际代码库中是如何工作的。

![Rasepi架构：插件、防护和管道协同工作](https://www.tcdev.de/zh/blog/img/architecture-pipeline.svg)

## 插件系统：模块化设计

Rasepi 中的每一个插件都实现了 `IPluginModule`_，这是一个单一的接口，它声明了插件是什么、需要哪些服务、暴露哪些路由：

__codeblock_0__

`PluginManifest` 是纯数据。它描述插件而不执行任何操作：

__codeblock_1__

注意 `UiContributions`。该字典将前端扩展点映射到组件名称，因此 Vue 前端知道每个插件贡献了哪些 UI 组件（工具栏按钮、侧边栏面板、设置页面）。

#### 每个插件只需注册一行

启动时，我们通过流畅的 API 注册插件：

__codeblock_2__

每次调用都会实例化模块，将其存储在注册表中，并调用 `RegisterServices()` 来连接其依赖关系。应用程序构建完成后，只需一行即可映射所有插件路由：

__代码块_3___

在引擎盖下，每个插件都会在 `/plugins/{pluginId}/` 处获得一个范围路由组，并自动应用授权。

#### 真实例子：工作流插件

下面是一个真实的插件，即工作流程与审批模块：

__codeblock_4__

核心平台从不直接引用 `WorkflowService`或 `WorkflowPublishGuard`。它是通过 DI 容器发现它们的。这就是零耦合的关键所在。核心应用程序从不接触插件代码。

## 操作防护：控制层

插件添加功能。操作保护器决定是否允许继续执行该功能或任何核心操作。它们是同步验证器，可在执行前拦截操作。

行动保护评估流程](https://www.tcdev.de/zh/blog/img/action-guard-flow.svg)

该接口特意做到了最小：

__codeblock_5__

当 _`ActionName`_为 _`null`_时，防护程序会在每个操作中运行。当设置为 `"Entry.Publish"`_时，它只拦截特定的操作。

#### 上下文和结果合约

每个 guard 都会收到一个类型化的上下文，其中包含操作名称、租户、用户、实体和一个属性包：

__codeblock_6__

每个监控程序都会返回一个可预测的结果：允许、拒绝或允许-带修改：

__代码块_7___

`Modifications` 字段非常重要。保护程序可以批准一项操作，但重写部分内容（例如，在发布前编辑机密）。

###规范操作名称

我们将所有可拦截的操作都定义为字符串常量，因此防护程序的目标操作不会有任何歧义：

__codeblock_8__

#### 真实例子：阻止未经批准的发布

工作流插件注册了一个拦截 _`Entry.Publish`的保护程序：

代码块_9__

核心平台对审批工作流一无所知。

### 操作管道：一切汇聚之处

`ActionPipeline` 是所有受保护操作的单一执行路径。它会确定哪些防护措施适用，对其进行评估，并阻止或执行操作。

__代码块_10___

_`EvaluateAsync`方法负责执行繁重的工作：

__代码块_11___

这里有三个重要的设计决定：

1.**`TenantPluginResolver`会检查每个租户安装并启用了哪些插件。禁用插件的防护程序永远不会运行。
2.2. **全部必须通过。** 如果任何防护措施被拒绝，该操作就会被阻止。这是故意采取的安全立场。
3.**如果防护程序出现异常，它将被记录并被视为 `Allow()`。这可以防止损坏的插件锁定整个平台。

### 每租户插件解决方案

解析器会查询 `TenantPluginInstallations` 表（通过 EF 全局查询过滤器自动扩展到当前租户）：

__代码块_12___

### 事件驱动的副作用

操作是同步的。而副作用不是。操作完成后，服务会发布一个域事件：

__codeblock_13__

事件被挂起到内存通道，并由后台 _________________________________________________处理。Worker 将事件路由到多个系统：

- ** 活动跟踪 ** 记录谁在何时做了什么。
- 跟踪每个提供商的成本
- 任何插件都可以订阅域事件

插件事件处理程序实现 `IPluginEventHandler`：

__编码块_14___

Worker 只调用租户已启用插件的处理程序。这意味着插件 A 的副作用永远不会泄漏到只安装了插件 B 的租户中。

## 块级翻译引擎

这是该架构最明显的优势所在。

块级翻译：仅对已更改的块进行重译](https://www.tcdev.de/zh/blog/img/block-translation.svg)

传统平台翻译整个文档。我们翻译的是单个**块**：段落、标题、列表项。当用户编辑 50 块文档中的一个段落时，只有该段需要重新翻译。这就是我们节省 94% 成本的原因所在。

### 如何从 TipTap JSON 创建块

当用户保存文档时，TipTap 编辑器会发送这样的 JSON：

__codeblock_15__

`BlockTranslationService`_会解析该 JSON 并创建单独的`EntryBlock`_记录：

__代码块_16___

### SHA256 哈希算法用于过期检测

内容散列是过期检测的核心。我们使用 SHA256 对区块内容（去掉 `blockId` 和 `deleted` 等元数据属性后）进行散列：

__代码块_17___

当源代码块发生变化时，它的哈希值也会发生变化。然后，系统会将每个翻译块的 `SourceContentHash`_与当前的源哈希值进行比较，不匹配的部分会被标记为 `Stale`_：

__代码块_18___

### 结构适应

翻译人员可以改变不同语言的代码块类型。英语的项目列表可能会变成德语的编号列表，这是一种文化偏好。系统会对此进行跟踪：

__codeblock_19__

### 翻译提供者作为插件

外部翻译服务（DeepL、Google Translate 等）通过 _`ITranslationProviderPlugin`_插入：

__代码块_20___

批处理方法接收内容块 ID 词典，翻译所有内容块，并返回翻译结果和计费字符数。由于我们只发送陈旧的块，而不是整个文档，因此成本保持在最低水平。

## 租户隔离：无形的安全网

上述每个系统都在严格的租户隔离内运行。

`TenantContextMiddleware` 会根据每次请求中的 JWT 解析租户，并验证成员身份：

__代码块_21___

Entity Framework 全局查询过滤器可确保即使开发人员忘记按租户进行过滤，数据库层也会自动进行过滤：

代码块_22___

结果：`db.Hubs.ToListAsync()` 总是只返回当前租户的集线器。数据泄露需要主动绕过查询过滤器，这在我们的代码库中是禁止的。

## 完整图片

当用户点击 "发布 "条目时，会发生以下情况：

1.**请求进入** 身份验证验证 JWT，`TenantContextMiddleware`_解析并验证租户。
2.**控制器调用管道** `IActionPipeline.ExecuteAsync("Entry.Publish", context, action)`
3.** 管道解析保护** 查询租户启用了哪些插件，并选择适用的保护。
4.4. **防护装置进行评估。** 工作流防护装置检查审批，保留防护装置检查策略，规则防护装置验证内容。全部通过？条目已发布。
5.5. **事件触发** _CODEBLOCK_54__事件被触发。后台工作者记录活动、更新翻译计费并调用插件事件处理程序。
6.**检查块翻译** 过时块将被识别，以便重新翻译。

各层各司其职。各层各司其职，互不影响。这就是架构。

> 我们建立这个平台并不是因为可扩展性很时髦。我们之所以构建它，是因为一个不能适应每个团队工作流程的文档平台最终会被一个能适应的平台所取代。而一个没有护栏的平台最终会破坏一些重要的东西。
