# 2. 先建立一个最小认知

> **导读** 动手写代码之前，有必要先厘清几个基本概念：插件究竟是什么，它由哪几个部分构成，系统是如何把它识别出来的，以及组件签名为何如此关键。本章不写任何代码，但它会帮你建立一张足够清晰的概念地图——有了这张地图，后续每一行代码的背后才会有迹可循。

在真正开始写插件之前，先做一件重要的事：把"插件"这个词从模糊的印象，变成一个可以落到代码里的具体对象。

很多人第一次接触插件系统时，会形成一种朴素的理解：**插件就是一堆额外的代码**。这句话不能说错，但实际上没什么用。只要你追问一句"那系统怎么知道哪些代码算插件、哪些不算"，这个说法便立刻不够了。

对 Neo-MoFox 来说，插件不是一堆零散功能，而是一组**被系统识别、被运行时加载、并能与其他组件协同工作**的扩展单元。换句话说，插件的重点不只是"你写了什么"，而是"系统如何理解你写的这些东西"。

### 2.1 什么是插件

先给出一个足够准确、又不过于绕口的定义：

> **插件是 Neo-MoFox 中一类可被发现、可被加载、可携带多个组件共同工作的扩展单元。**

这个定义里有三个词值得特别留意：

- **可被发现**：系统得先在插件目录里找到它。
- **可被加载**：不只是看到它，还要将其作为 Python 模块导入，并实例化对应的插件类。
- **可携带多个组件共同工作**：插件本身通常不是执行功能的最小单位，真正承担具体职责的，是它内部声明的各类组件。

所以当你说"我要写一个插件"时，更准确的含义其实是：

> 我要为 Neo-MoFox 定义一个新的扩展单元，并在这个单元中声明若干组件，让系统能够在运行时正确装载并调用它们。

先接受这个视角，后面很多设计便会水到渠成。因为这意味着插件不是孤立脚本，而是系统中的一个正式成员。

### 2.2 一个插件，最少由哪些部分组成

一个可以正常工作的插件，至少可以从下面几个部分来理解：

#### **1. 插件目录**

插件首先得存在于系统可扫描的位置。默认情况下，Neo-MoFox 会从项目根目录下的 `plugins` 目录发现插件。

这一步解决的是一个基础但现实的问题：**系统去哪里找你写的东西。**

#### **2. 插件清单 manifest.json**

清单文件负责告诉系统：

- 这个插件叫什么。
- 它的版本和描述是什么。
- 它的入口文件在哪里。
- 它依赖哪些插件或组件。
- 它要求的最低核心版本是什么。

可以先把 `manifest.json` 理解成插件的"身份证"和"说明卡片"。它不负责执行功能，但负责在装载插件之前，先让系统知道这个插件的基本信息。

#### **3. 插件类**

真正把插件"落成代码对象"的，是继承自 `BasePlugin` 的插件类。

它通常承担以下几件事：

- 声明自身的名称、描述和版本。
- 通过装饰器将自己注册到系统，使其可被发现。
- 返回自己包含的组件列表。
- 在需要时处理加载和卸载阶段的生命周期逻辑。

如果把 `manifest.json` 看成身份证，那么插件类更像这个插件在运行时的"实体"。

#### **4. 组件类**

组件才是大多数实际功能的承载者。命令、工具、服务、事件处理器、对话逻辑、适配器，这些都属于组件。

插件可以理解为一个容器，组件是容器里真正分工协作的部分。

这也是为什么在 Neo-MoFox 里，我们更倾向于说"写一个插件，并在其中组织多个组件"，而不是"写一个大类把所有事都包办"。

#### **5. 配置类（可选，但通常必要）**

严格来说，不是每个插件都必须带配置；但只要插件开始有可调行为、可选开关、阈值、路径、平台参数，配置几乎就会变成必需品。

在当前实现里，配置类不通过 `get_components()` 返回，而是通过插件类上的 `configs` 属性声明，让插件管理器优先加载并注入到插件实例中。

这一点值得提前记住，因为它会直接影响你后面组织代码的方式。

### 2.3 插件、组件、配置、清单分别扮演什么角色

如果读到这里你仍觉得这几个概念挤在一起，可以先用下面这张关系表来整理：

- **manifest**：告诉系统"我是谁，我在哪，从哪里进"。
- **插件类**：告诉系统"这个插件在运行时应该如何被实例化"。
- **组件类**：告诉系统"这个插件具体提供哪些能力"。
- **配置类**：告诉系统"这些能力有哪些可调参数"。

你会发现，这四者各自回答了一个不同的问题：

- **身份问题**：这个插件是谁。
- **入口问题**：系统从哪里把它加载进来。
- **能力问题**：它到底能干什么。
- **行为问题**：它在不同场景下该怎么表现。

把这几个问题分开来看，插件的整体结构便会清晰许多。

初学者容易不自觉地把这些职责揉成一团，比如：

- 让 manifest 承担运行时逻辑。
- 让插件类承载全部业务代码。
- 让组件既当入口又当服务又当状态容器。

这些写法短期看似乎更省事，但插件一旦稍微长大，就会开始变得难读、难测、难扩展。后面几章我们会不断回头处理这个问题。

### 2.4 系统是怎么识别一个插件的

这是整个插件系统里最值得建立直觉的一步。

目前，系统识别一个插件，大致会经历下面这条路径：

1. **扫描 plugins 目录**，寻找包含 `manifest.json` 的插件目录，或带有 manifest 的 zip、mfp 插件包。
2. **读取 manifest**，拿到插件名、入口文件、依赖、最低核心版本等信息。
3. **导入插件入口模块**，在导入过程中触发 `@register_plugin` 装饰器，把插件类注册进全局插件注册表。
4. **根据插件名取回插件类**，实例化插件对象。
5. **加载 configs 中声明的配置类**，并把配置实例注入插件。
6. **调用插件类的 `get_components()`**，拿到这个插件真正要注册的组件。
7. **为每个组件推断类型、生成签名、注册到全局组件注册表**。

如果想用一句话概括这整段流程，可以记成：

> **manifest 先让系统找到插件，插件类再告诉系统要注册哪些组件。**

这是一个简化描述，但对理解当前实现已经足够准确。

### 2.5 什么是组件签名，为什么它这么重要

在 Neo-MoFox 里，组件不是只靠类名区分的，而是靠一套明确的签名格式来唯一标识：

```text
plugin_name:component_type:component_name
```

比如：

```text
my_plugin:command:hello
my_plugin:service:memory
other_plugin:tool:calculator
```

这套签名有两个关键作用：

- 让系统能唯一地识别一个组件。
- 让依赖关系可以被清晰表达和检查。

后续你会频繁在以下场景中看到它：

- 组件注册。
- 依赖声明。
- 状态管理。
- 组件查找。
- 插件间协作。

不要把组件签名当成一种"内部实现细节"——它实际上是整个插件系统中各部分相互协作时共用的坐标系。

### 2.6 PluginLoader 和 PluginManager 的分工

这一点如果能早点理解，后面读代码会轻松很多。

Neo-MoFox 当前把插件加载拆成了两层：

- **PluginLoader** 负责宏观层面：发现插件、读取 manifest、检查版本和依赖、规划加载顺序。
- **PluginManager** 负责具体执行：导入单个插件模块、找到插件类、实例化插件、注册组件、调用生命周期钩子。

可以把它们理解为两种不同层级的职责：

- Loader 更像"调度与规划"。
- Manager 更像"执行与落地"。

这个拆分的价值在于把"决定该不该加载"与"实际怎么加载"分离开来。系统一旦复杂起来，这种分层会显著降低维护难度。

### 2.7 什么时候应该写插件，什么时候不必写

这是入门阶段值得提前想清楚的一个问题。

适合写插件的场景，通常包括：

- 要给 Bot 增加一块相对独立的新能力。
- 这块能力未来可能继续扩展，或者需要单独维护。
- 它需要与现有组件系统、配置系统、事件系统、对话系统协同工作。
- 希望它未来可以被复用、被启用、被禁用、被替换。

不一定需要单独写成插件的场景，通常是：

- 只是一次性的实验脚本。
- 只是修改某个现有插件内部的一小段逻辑。
- 只是非常局部、且不会被复用的临时功能。

一个实用的判断方法：

> 这块功能以后会不会被当成一个独立能力来看待？

如果答案是"会"，那它大概率值得拥有自己的插件边界。

### 2.8 常见组件类型可以先怎么理解

本节不要求你立刻会写，只是帮你先建立一张"组件地图"。

#### **Command**

面向用户输入的命令入口，比如 `/help`、`/config`、`/remember` 这类显式触发的功能。

适合场景：通过明确命令让用户调用某项能力。

#### **Service**

面向复用的业务能力封装，适合承载核心逻辑，并被其他组件或插件调用。

适合场景：某段逻辑不应只服务于一个命令，而应成为一块可复用的能力。

#### **Tool**

面向模型调用的工具接口，通常用于让对话系统在需要时查询信息或执行动作。

适合场景：某项能力需要由模型在运行时按需调用。

#### **EventHandler**

对系统事件作出响应的组件，不依赖用户显式输入，而是由事件触发。

适合场景：系统启动、消息到达、插件加载完成等时机需要触发特定行为。

#### **Chatter**

定义对话行为和回复决策的组件，更像一个"对话大脑"。

适合场景：需要处理持续性的对话能力，而不只是单点命令。

#### **Agent**

更复杂的任务编排组件，适合处理多步骤推理、代理式调用或更长链路的能力组合。

适合场景：一个请求无法靠单次函数调用解决，需要流程化处理。

#### **Router**

对外暴露接口的组件，通常用于 HTTP 路由等外部访问入口。

适合场景：插件能力需要被系统外部访问。

#### **Adapter**

连接平台消息协议与系统内部消息模型的桥梁组件。

适合场景：需要对接一个新的聊天平台或传输协议。

#### **Action**

偏执行型的能力组件，承担"执行一个动作"的职责，比如发送消息、结束对话等。

适合场景：对话系统已决定要做什么，接下来需要一个可执行动作将其落实。

#### **Config**

虽然它不总被当作"功能组件"来讨论，但从系统视角看，它同样是插件能力组织的一部分。

适合场景：插件存在开关、参数、阈值、路径、模式等可配置行为。

### 2.9 先别急着记全，先记住这条主线

初学阶段，只需先记住这一条主线：

> **插件是容器，组件是职责单元，manifest 是说明入口，配置决定行为细节。**

等你开始亲手写第一个插件时，这条主线会比死记每个类名更有用。

后面我们不会一下子把所有组件类型都写一遍，而是先从最容易获得正反馈的部分开始。你会先做出一个最小插件，再逐步理解为什么它要长成那样。

> **提示**
>
> 当前实现中，插件真正注册哪些组件，主要由插件类的 `get_components()` 与 `configs` 决定。manifest 中的 `include` 更适合理解为面向依赖结构设计的一层声明，而不是理解加载流程时必须掌握的唯一入口。

到这里，你已经不再只是知道"插件这个词大概是什么意思"，而是开始了解**系统究竟靠什么把一个插件认出来**。有了这个基础，进入下一章的节奏就刚刚好——接下来，我们终于可以动手，做第一个真正能跑起来的插件了。
