插件系统

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

作者 David更新于 第 26 篇(共 33 篇)

插件是 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.jsonAgent Plugins 1.0.0$schema 必须是 agent-plugins.org 的 1.0.0 schema;最多 64 个技能目录、8 个 MCP 服务器;不带钩子
.minimax-plugin/plugin.jsonMiniMaxschemaVersion 必须为 1,未知字段直接拒绝
兼容 Claude Code 的清单Claude Code缺省读 hooks/hooks.json 与根目录的 .mcp.json
.codex-plugin/plugin.jsonCodex与上一种共用读取器

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

  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 清单大致如下:

{
  "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 拼成“插件名 + 冒号 + 技能名”,见技能
MCP 服务器*.mcp.json独立的插件 MCP 运行时,见下一篇
钩子*.json在回合的固定节点跑命令,见下文
App*.app.json,只写一个 provider只对官方包读取,把插件与云端连接器关联起来,见账号与云端工具
宿主绑定bindings/*.binding.json声明复用宿主已有的能力,仅限 interactive 表面
MiniApppackage.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,见浏览器工具)。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 服务器则由命名名册另配一个带后缀的名字。

图表加载中…

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

    // 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 格式的钩子文件大致如下:

{
  "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):

      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)。

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

可对照 OpenCode 的插件系统与 Kimi Code 的插件与市场。MiniMax Code 的特点是除了自家清单,还直接读取 Agent Plugins、Claude Code、Codex 三种外部格式,钩子的输入输出也按各家约定分别投影。

下一篇:MCP——三层 MCP 配置、mcp__ 命名名册与按需检索的工具披露。

本页目录