# 插件系统

> 官方与本地两类插件源、四种清单格式、插件带来的技能与 MCP 与钩子等能力，快照怎样在回合之间发布，以及兼容 Claude Code 与 Codex 格式的钩子如何执行。

- 作者：David（道雾轩）
- 专栏：MiniMax Code 源码解读（https://daiw.org/manual/minimax-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/minimax-code/plugins
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

插件是 MiniMax Code 把技能、MCP 服务器、钩子等能力打包分发的单位，实现集中在 `packages/local-runtime-v2/src/service/plugin-system/`。这一目录的注释处处写着 Desktop，入口函数头上就是一句：

> Assembles the Desktop Plugin service over narrow v1 compatibility ports.

（`packages/local-runtime-v2/src/service/plugin-system/initialize.ts:27`）但它并不专属桌面端：`packages/local-runtime-v2/src/services.ts:656` 组装运行时服务时无条件调用 `initializePluginService`，终端里的 `mcode` 跑的是同一套插件系统，只是暴露出来的入口少一些。

## 怎么用

命令行入口定义在 `packages/tui/src/cli/program.ts:271` 起的一段：

| 命令 | 作用 |
| --- | --- |
| `mcode plugin` | 启动 TUI 并直接打开 `/plugins` 面板 |
| `mcode plugin list` | 列出已装插件；`--available` 连同可安装的一起列，`-m` 只看 `official` 或 `local`，`--json` 输出 JSON |
| `mcode plugin add <plugin[@marketplace]>` | 安装；`remove`、`enable`、`disable` 三个子命令写法相同 |
| `mcode plugin marketplace list` | 列出两个来源：`official`（云端 registry）与 `local`（本地目录） |
| `mcode plugin marketplace upgrade` | 刷新全部来源 |

插件名后面可以带 `@official` 或 `@local`；同名插件在两边都有时必须写明，否则报错（`packages/tui/src/cli/plugin-command.ts:160`）。TUI 里的 `/plugins [filter]` 是同一套操作的面板：四个标签页 All Plugins、Installed、MiniMax Official、Local 用 Tab 切换，Enter 安装、Space 启用或停用、Delete（或 Ctrl+D）卸载、Ctrl+R 刷新（`packages/tui/src/tui/features/plugin/manager.ts:176`）。`/reload` 会重读配置并刷新插件（`packages/tui/src/tui/launcher.ts:445`）。

两类来源的操作方式不同：

- **官方插件**必须登录。未登录时安装报 `PLUGIN_AUTH_REQUIRED`（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/desktop-facade.ts:328`），面板提示先 `/login`。
- **本地插件**不能“安装”，`add` 一个本地插件直接报 `LOCAL_PLUGIN_INSTALL_UNSUPPORTED`（`desktop-facade.ts:364`）。做法是把插件目录放进数据目录下的 `plugins/`：数据目录默认 `~/.minimax-code`，可用 `MINIMAX_DATA_DIR` 或 `MAVIS_DATA_DIR` 改（`packages/tui/src/runtime/data-dir.ts:20`）。放进去即被发现并默认启用；`remove` 会把这个目录挪进隔离区，发布成功后删掉（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/package-storage.ts:470`）。

## 两个来源

### 官方：跟着账号走的云端状态

官方插件的安装状态以服务端为准。客户端访问 `/minimax-cloud/api/v1` 下的一组接口（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/registry-client.ts:26`），域名在国内版是 `https://agent.minimax.cn`，国际版是 `https://agent.minimax.io`，也可用 `NEXT_PUBLIC_DOMAIN_URL` 覆盖（`initialize.ts:213`）。本地状态按“用户 ID + 部署”分区（`packages/local-runtime-v2/src/service/plugin-system/plugin-system.ts:1165`），没登录就没有分区，官方列表为空；换账号时整份快照随之切换。

同步分两步：先 `GET /plugins/full-state` 拿到应生效的包清单，每个包带 `archiveSha256` 与 `sha256-tree-v1:` 前缀的内容摘要；缓存里没有的包再 `POST` 一次下载授权，拿到带时效的 `downloadUrl` 下载 zip，校验两个摘要后解到 `<dataDir>/v2/plugin-cache/official/` 下以摘要命名的目录（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/official-reconciler.ts:246`）。有包失败时这次同步记为不完整，按 30、60、120、240、300 秒退避重试（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/official-operations.ts:277`）。包的上限写在 `PLUGIN_PACKAGE_V1_LIMITS`：压缩包 64 MiB、最多 1024 个文件、单文件 16 MiB（`packages/local-runtime-v2/src/service/plugin-system/plugin/package/package-contract.ts:9`）。

仓库外的官方插件仓库 MiniMax-AI/MiniMax-Code-Plugins 在代码里没有出现，客户端能看到的只有上面这组云端接口。另一个细节：这条传输在 CLI 里也把公共参数写成 `client: 'desktop'`、`is_desktop: 1`（`packages/local-runtime-v2/src/service/plugin-system/cloud-transport.ts:191`）。

### 本地：数据目录下的插件目录

本地扫描 `<dataDir>/plugins` 的直接子目录，跳过符号链接，并发 4 个读取（`packages/local-runtime-v2/src/service/plugin-system/plugin/package/package-readers.ts:22`）。目录用 `node:fs` 的 `watch` 递归监视，变化经 150 毫秒去抖后触发一次重建（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/local-directory-watcher.ts:11`）。停用本地插件时把它规范化后的根路径写进 SQLite 表 `local_runtime_plugin_local_disabled`；官方状态则整份 JSON 存在 `local_runtime_plugin_official_state`（`packages/local-runtime-v2/src/infra/db/schema/plugin.ts:4`）。

`PluginSystem` 还有第三个入口 `importGithubPlugin`（`plugin-system.ts:515`）：从 GitHub 或其他 HTTPS Git 仓库按完整 commit SHA 拉取，导入成本地插件。但它只挂在桌面端门面 `PluginDesktopFacade` 上，CLI 用的 `CliService` 只有刷新、两种列表与增删启停共七个插件方法（`packages/local-runtime-v2/src/local/cli-service.ts:594`），`mcode` 里没有从 GitHub 导入插件的途径。

## 四种清单

一个本地插件目录可以用四种清单之一，识别顺序见 `package-readers.ts:146`：

| 清单位置 | 格式 | 要点 |
| --- | --- | --- |
| 根目录 `plugin.json` | Agent Plugins 1.0.0 | `$schema` 必须是 agent-plugins.org 的 1.0.0 schema；最多 64 个技能目录、8 个 MCP 服务器；不带钩子 |
| `.minimax-plugin/plugin.json` | MiniMax | `schemaVersion` 必须为 1，未知字段直接拒绝 |
| 兼容 Claude Code 的清单 | Claude Code | 缺省读 `hooks/hooks.json` 与根目录的 `.mcp.json` |
| `.codex-plugin/plugin.json` | Codex | 与上一种共用读取器 |

根目录的 `plugin.json` 只有被 Agent Plugins 读取器完整接受时才算数，否则继续尝试后三种（`package-readers.ts:150`）：

```ts
  if (await pluginPathExists(root, 'plugin.json', 'file')) {
    const agentPlugin = await tryReadAgentPluginPackage(rootPath);
    if (agentPlugin) return agentPlugin;
  }
  const compatibleManifestPath = '.\u0063\u006c\u0061\u0075\u0064\u0065-plugin/plugin.json';
  const manifests = {
    hasMiniMax: await pluginPathExists(root, '.minimax-plugin/plugin.json', 'file'),
    hasCompatible: await pluginPathExists(root, compatibleManifestPath, 'file'),
    hasCodex: await pluginPathExists(root, '.codex-plugin/plugin.json', 'file'),
  };
  if (!manifests.hasMiniMax && !manifests.hasCompatible && !manifests.hasCodex) return undefined;
  return readCandidatePackage(rootPath, manifests);
```

兼容清单的路径写成了 Unicode 转义，逐字解码就是 Claude Code 插件的 .claude-plugin 目录；来源枚举里对应的值也是这样拼出来的（`packages/local-runtime-v2/src/service/plugin-system/plugin/package/types.ts:22`）。而在 `packages/local-runtime-v2/src/service/plugin-system/plugin/package/compatibility-reader.ts:22`，常量名 `CLAUDE_MANIFEST_PATH` 与 `kind` 的取值 `'CLAUDE_CODE'` 是明文，只有字符串值做了转义，代码没有解释这种写法。

MiniMax 自己的清单最严格，字段白名单见 `packages/local-runtime-v2/src/service/plugin-system/plugin/package/minimax-reader.ts:46`，各项能力都以相对路径引用文件，路径形状用正则卡死。名字须以小写字母开头、只用小写字母数字与 `.`、`_`、`-`；`version` 必须是 semver；`category` 只能取十个市场分类之一；一项能力都没有时报 `MANIFEST_SCHEMA_INVALID`（`minimax-reader.ts:131`）。

一个最小的 MiniMax 清单大致如下：

```json
{
  "schemaVersion": 1,
  "name": "repo-guard",
  "version": "1.0.0",
  "description": "Guard risky shell commands",
  "author": "you",
  "icon": "assets/icon.png",
  "category": "Code",
  "exampleQueries": [],
  "apps": [],
  "mcpServers": ["servers/guard.mcp.json"],
  "skills": ["skills/review/SKILL.md"],
  "hooks": ["hooks/guard.json"]
}
```

## 插件能带来什么

| 能力 | 清单里的写法 | 去向 |
| --- | --- | --- |
| 技能 | `skills/<name>/SKILL.md` | 运行时名由 `buildPluginSkillRuntimeName` 拼成“插件名 + 冒号 + 技能名”，见[技能](https://daiw.org/manual/minimax-code/skills) |
| MCP 服务器 | `*.mcp.json` | 独立的插件 MCP 运行时，见[下一篇](https://daiw.org/manual/minimax-code/mcp) |
| 钩子 | `*.json` | 在回合的固定节点跑命令，见下文 |
| App | `*.app.json`，只写一个 `provider` | 只对官方包读取，把插件与云端连接器关联起来，见[账号与云端工具](https://daiw.org/manual/minimax-code/accounts-and-tools) |
| 宿主绑定 | `bindings/*.binding.json` | 声明复用宿主已有的能力，仅限 `interactive` 表面 |
| MiniApp | `package.json` 的 `mcode` 字段 | 桌面端的小应用，CLI 不运行 |

App 只在 `options.source === 'OFFICIAL'` 时才读（`minimax-reader.ts:81`），连接器工具本身每个回合从云端现取。宿主绑定的类型注释说得明白：包里不带任何实现，声明本身也不授予权限，是否兑现由宿主策略决定（`types.ts:69`）；生产组装里只注册了浏览器这一个宿主能力（`packages/local-runtime-v2/src/service/turn-system/production-composition.ts:761`，见[浏览器工具](https://daiw.org/manual/minimax-code/browser-use)）。MiniApp 由 `packages/local-runtime-v2/src/service/miniapp` 的监督器托管，而 `resolveRuntimeMiniAppOptions` 只在宿主是 `electron` 时返回配置（`packages/local-runtime-v2/src/service/miniapp/composition.ts:29`）；TUI 以 `runtimeOwnerKind: 'tui'` 启动运行时（`packages/tui/src/runtime/embedded-host.ts:84`），监督器根本不会创建，插件里的 MiniApp 在 CLI 中只被读进快照、不会运行。

## 快照与发布：插件怎样进入回合

`PluginSystem` 实现了回合系统的能力提供者接口：它不直接改工具表，而是把全部插件编译成一份不可变的快照。构建时先放官方包、后放本地包，所以重名时官方胜出，本地的记 `LOCAL_PLUGIN_NAME_CONFLICT`（`packages/local-runtime-v2/src/service/plugin-system/plugin/runtime/snapshot-builder.ts:294`）。独立技能名与 `mcp.json` 里已启用的服务器名事先登记进来（`initialize.ts:231`）：与之重名的插件技能被丢弃，重名的 MCP 服务器则由命名名册另配一个带后缀的名字。

```mermaid
flowchart LR
  O["官方：full-state 与缓存包"] --> B["PluginSnapshotBuilder"]
  L["本地：plugins 目录"] --> B
  R["登记名：独立技能与 mcp.json"] --> B
  B --> P["待发布的快照"]
  P -->|"tryPublish：没有发布在进行"| G["当前能力代"]
  G -->|"acquire：捕获并租用"| T["新回合"]
```

新快照先排队，再经回合系统的发布口 `tryPublish` 原子地换上（`plugin-system.ts:1032`）。回合开始时由 `AgentHostTurnCapabilityLifecycle` 捕获当时的能力视图并登记租约，这一步刻意不跨 `await`（`packages/local-runtime-v2/src/service/turn-system/agent-host/assembly/turn-capability-lifecycle.ts:137`）：

```ts
    // No await below this point: capture + registration is one JS critical section.
    const capabilities =
      provider && input.captureCapabilities !== false
        ? provider.captureTurnCapabilities(preparation)
        : undefined;
    if (capabilities) provider?.retainTurnCapabilities?.(capabilities);
    const token = {};
    const turn = {
      sessionId: input.sessionId,
      turnId: input.turnId,
      ...(capabilities ? { capabilities } : {}),
    };
    this.inFlight.set(token, turn);
```

于是正在跑的回合始终用它开场时的那一代插件：新装的插件从下一个回合起才可见；被换下来的插件 MCP 运行时要等最后一个租用它的回合结束才关闭（`packages/local-runtime-v2/src/service/plugin-system/mcp/generation-registry.ts:15`）。停用或卸载插件时还会通知钩子协调器，让该插件的钩子在会话里失效。

## 钩子

钩子的解析、执行与会话生命周期在 `packages/agent-modules/plugin-hooks`；喂给钩子的转录与工具返回值投影在 `packages/agent-tools/src/plugin-hooks`。支持 11 个事件（`packages/agent-modules/plugin-hooks/src/contracts.ts:2`）：

| 事件 | 触发时机 |
| --- | --- |
| `SessionStart` | 插件的钩子在某个会话里第一次生效时，随该回合一起跑；来源有 `startup`、`resume`、`clear`、`compact`、`fork` 等 |
| `UserPromptSubmit` | 每个回合开始；可拒绝本次输入或补充上下文 |
| `PreToolUse` / `PostToolUse` | 工具调用前后；前者可放行、拒绝、改写参数，后者可追加上下文或替换模型看到的结果 |
| `PermissionRequest` | 权限闸门准备询问用户时；钩子可以直接批准或拒绝 |
| `Stop` / `SubagentStop` | 回合结束；返回 `block` 可让 Agent 接着干 |
| `SubagentStart` | 子 Agent 启动 |
| `PreCompact` / `PostCompact` | 压缩前后 |
| `SessionEnd` | 会话空闲 30 分钟，或归档、清空、切走、登出 |

每个钩子带一个来源格式：`'MINIMAX' | 'CLAUDE' | 'CODEX'`（`contracts.ts:18`），分别对应自家清单、Claude Code 与 Codex 两类兼容清单，注释里把 Claude Code 那一类称为 Compatible。解析规则在 `packages/agent-modules/plugin-hooks/src/parser.ts`：只支持 `type: "command"`，`async` 或 `asyncRewake` 为真的异步钩子一律不收；`timeout` 以秒计、只能取 1 到 10，缺省 5 秒；每个插件最多 64 个可执行钩子，matcher 最长 256 个字符（`parser.ts:10`）；Windows 上优先取 `commandWindows`。

一个 MiniMax 格式的钩子文件大致如下：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "bash",
        "hooks": [{ "type": "command", "command": "node ./hooks/guard.mjs", "timeout": 5 }]
      }
    ]
  }
}
```

执行器 `PluginHookRunner` 把事件序列化成 JSON 写进子进程的 stdin，同一事件最多 8 个命令并发，普通事件总预算 15 秒、`SessionEnd` 只有 3 秒（`packages/agent-modules/plugin-hooks/src/runner.ts:36`）。子进程不继承完整环境，只拿 `PATH`、`HOME` 等十几个白名单变量，再加上三家各自约定的变量（`runner.ts:239`）：

```ts
      const env = {
        ...safeHookEnvironment(),
        MINIMAX_PLUGIN_ROOT: handler.pluginRoot,
        CLAUDE_PLUGIN_ROOT: handler.pluginRoot,
        CODEX_PLUGIN_ROOT: handler.pluginRoot,
        PLUGIN_ROOT: handler.pluginRoot,
        CLAUDE_PLUGIN_DATA: pluginDataDir,
        PLUGIN_DATA: pluginDataDir,
        MINIMAX_PROJECT_DIR: input.cwd,
        CLAUDE_PROJECT_DIR: input.cwd,
        CODEX_PROJECT_DIR: input.cwd,
        ...(handler.sourceFormat === 'CLAUDE' && handlerInput.effort
          ? { CLAUDE_EFFORT: handlerInput.effort.level }
          : {}),
      };
```

几条值得记住的语义：

- **失败即放行**：超时、崩溃、输出非法的钩子都只记一条诊断并跳过，日志原文就是“failed open”（`runner.ts:125`）；转录里会出现一条运行时警告。
- **退出码 2 只对兼容格式有意义**：Claude Code 与 Codex 格式的钩子以 2 退出时，stderr 作为拦截理由；MiniMax 格式不看退出码（`runner.ts:311`）。
- **多个钩子取最严**：决定按 `allow`、`ask`、`defer`、`deny` 由弱到强合并（`runner.ts:1541`）。
- **Shell 选择**：Claude Code 格式的 shell 形式命令在 Windows 上先找 Git Bash，找不到再退到 PowerShell（`packages/agent-modules/plugin-hooks/src/command-invocation.ts:55`）。
- **转录投影**：兼容钩子常读 `transcript_path`，运行时为此在系统临时目录的 `minimax-plugin-hooks/transcripts` 下维护两份 JSONL 投影，单文件 8 MiB、目录 64 MiB、最长保留 24 小时（`packages/agent-tools/src/plugin-hooks/transcript.ts:13`）。
- **终端控制序列**只在真正的 TUI 里执行，其他表面只收到一句提示（`packages/local-runtime-v2/src/service/turn-system/agent-host/assembly/local-turn-plugin-hooks.ts:96`）。

整个进程共用一个钩子协调器 `localPluginHookCoordinator`（`local-turn-plugin-hooks.ts:37`）。`SessionEnd` 的 30 分钟空闲计时器调用了 `unref()`，不会为了等它而拖住进程退出（`packages/agent-modules/plugin-hooks/src/coordinator.ts:1073`）。

<Callout type="info">
  CLI 与桌面端共用插件引擎，差别在入口：CLI 只管理官方与本地两类来源，没有 GitHub 导入，也不运行 MiniApp。仓库的 `README_ZH.md` 与 `docs/tui-capabilities.md` 都把“官方 / 本地 / GitHub 插件”列为已恢复的能力，就 `mcode` 而言要打个折扣。
</Callout>

可对照 [OpenCode 的插件系统](https://daiw.org/manual/opencode/plugins)与 [Kimi Code 的插件与市场](https://daiw.org/manual/kimi-code/plugins)。MiniMax Code 的特点是除了自家清单，还直接读取 Agent Plugins、Claude Code、Codex 三种外部格式，钩子的输入输出也按各家约定分别投影。

下一篇：[MCP](https://daiw.org/manual/minimax-code/mcp)——三层 MCP 配置、`mcp__` 命名名册与按需检索的工具披露。
