musepi-extension-dev · git:20261102.a583336 · 2026-11-02 · sha256 931f02ef6be43c5a
musepi-extension-dev git:20261102.a583336A
Immutable. This exact content never changes and is served at /api/v1/blob/931f02ef6be43c5a.
---
name: musepi-extension-dev
description: 为 MusePi(TUI/CLI + 桌面 GUI)开发扩展——custom tools、extensions(生命周期/事件)、hooks、plugins、slash commands、skills。触发:用户要求"加个工具/扩展/命令/hook/插件"、"给 musepi 加 X 支持"、修改扩展加载机制、写 MCP 相关集成。桌面 GUI 会话(daemon)与 TUI 共享同一扩展系统,扩展天然两形态可用。
---
# MusePi 扩展开发
扩展系统在 `packages/coding-agent/src/extensibility/`。六种形态,各有用途:
| 形态 | 是什么 | 关键文件 |
|---|---|---|
| **Custom tool** | 模型可调用的函数(execute + Zod schema) | `extensibility/custom-tools/loader.ts` |
| **Extension** | 生命周期/事件框架——可注册工具、拦截/修改事件 | `extensibility/extensions/`(loader/runner) |
| **Hook** | 外部 pre/post 命令脚本(如 tool_call 拦截) | `extensibility/hooks` |
| **Plugin** | 可安装的扩展包(`musepi.extensions` 清单,`omp`/`pi` 遗留兼容) | `extensibility/plugins` |
| **Slash command** | 斜杠命令(`/xxx`) | `extensibility/slash-commands.ts` |
| **Skill** | 静态知识包(SKILL.md) | `extensibility/skills.ts` + `discovery/builtin.ts` |
## 快速路径:最小扩展(够用 90% 场景)
写一个 TS 文件(默认导出 factory),放到 `~/.musepi/agent/extensions/<name>/index.ts`(用户级)或 `<cwd>/.musepi/extensions/<name>/index.ts`(项目级):
```ts
import type { ExtensionAPI } from "@musepi/pi-coding-agent";
export default function (pi: ExtensionAPI) {
const z = pi.zod;
pi.setLabel("My Extension");
pi.on("tool_call", async (event) => {
if (event.toolName === "bash" && /rm -rf/.test(event.input.command ?? "")) {
return { block: true, reason: "Blocked by extension policy" };
}
});
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "…",
parameters: z.object({ query: z.string() }),
async execute(_id, params, _signal, _onUpdate, _ctx) {
return { content: [{ type: "text", text: `got ${params.query}` }] };
},
});
pi.registerCommand("my-cmd", {
description: "…",
handler: async (_args, ctx) => ctx.ui.notify("hi", "info"),
});
}
```
扩展在 **Bun 运行时**执行:ESM 模块(无 `require`)、顶层 `export default` factory、可用 `await import()` 按需加载依赖。扩展代码与其他扩展共享进程——**不要**在扩展里做长阻塞/全局状态污染。
## 加载与发现(怎么让 musepi 看到你的扩展)
1. **自动发现**:`~/.musepi/agent/extensions`(用户)+ `<cwd>/.musepi/extensions`(项目)——目录含 `index.ts` 即可
2. **显式配置**:`~/.musepi/agent/config.yml` 的 `extensions:` 数组(路径/包目录)或 `-e/--extension` CLI flag
3. **禁用**:`disabledExtensions: [extension-module:<name>]`(name = 目录/文件名);`--no-extensions` 全关
4. **验证**:`musepi /extensions` 列出已加载扩展;扩展里的 `console.error` 进 daemon/TUI 日志
5. **改动生效(HMR 热拔插)**:改扩展**入口文件**后 daemon watcher ~500ms 内广播 `extensions.changed`,GUI ~1s 刷新,活跃会话执行 `reloadExtension`(忙会话挂起、agent_end 补做)——**不用重启 daemon**;会话内工具下次调用生效,未重注册的旧工具名被移除。注意:**子模块改动不热生效**(Bun 模块缓存只重键入口 specifier)——多文件扩展改了子模块要 touch 入口文件。扩展内存态不迁移。只有改了扩展的发现/加载机制本身、或 daemon 侧注册代码时才需要重启 daemon(GUI 菜单"重启 daemon");TUI 新会话即生效
## 插件配置(manifest config + pi.config,dsh 管理页 parity)
扩展需要用户可配项时,在扩展目录的 package.json 的 `musepi` 块(权威清单字段;`omp`/`pi` 遗留兼容可读)声明 `config` 字段表(`boolean | number | string | select | path`,数字可带 `min/max/step`,下拉必须给 `options`,每个字段可标 `restart: none|session|daemon` 生效域)与 `resources` 资源卡(`disk`/`memory`/`setupMinutes`/`models`)。扩展中心详情页自动渲染管理表单并落盘;运行时读取:
```ts
const threshold = await pi.config.get<number>("threshold"); // 钳制后的值,未写过 = 声明默认
const all = await pi.config.getAll(); // 完整值表(只含声明键)
```
取值与表单、写入共用同一条钳制链路:存储坏值回退声明默认,未声明键读不到。存储落点 `<agentDir>/extensions/plugin-config.json`,键 = `extension-module:<name>`。fail-soft:坏清单字段被逐个丢弃并给警告,不拖垮扩展登记。详见 `docs/extensions-dev.md` §13。
## 自举工具环(agent 在会话内自己管扩展)
daemon 会话给 agent 挂了一组扩展自举工具,让创造类任务不靠 GUI 就能完成"写文件 → 加载 → 自查 → 热修 → 回退"闭环:
- `extensions_list`——已加载扩展清单(**唯一常激活**,只读、便宜)
- `extension_load` / `extension_reload` / `extension_status` / `extension_validate` / `extension_rollback`——生命周期五件套;`extension_rollback` 回退到加载时的版本化快照(快照在 `~/.musepi/extension-backups`)
- `ext_define` / `ext_run` / `ext_stop` / `ext_undefine` / `ext_inspect`——会话级动态插件(vm 沙箱,`extdyn__` 命名空间),不写盘就能试原型
激活方式:普通会话里 `/extensions` 斜杠命令;**creator(创造)预设自动激活整环**(settings `tools.extensionMetaTools`,会话起即顶层可用,不走 xd:// 设备)。默认不激活的原因:普通会话不为这 10 个工具的 schema 付费。
## Cordis 插件运行时(组件 / 子 fiber / 兼容性预检)
- **组件**:插件 package.json `musepi.components` 声明组件数组(`[{ id, label?, deny? }]`);宿主为组件建独立子 fiber,设置页可对单组件启停(deny 列表落盘)。整插件 reload 时组件一并重建。
- **宿主 fiber 检视**:cordis 组 fiber 下各插件/组件的加载状态可从扩展检视入口查询(`extension_status` / 插件管理页)。
- **兼容性预检**:加载插件前宿主先做 Bun/平台兼容性检查;声明不兼容时插件详情弹窗给归因段,用户可对整个插件**授予精确版本豁免**(写 `<agentDir>/compatibility.json`,RPC `extensions.setVersionExemption`)——比 `bunfig` 豁免窄,只放过单个版本。
## 文档路由表(需要细节时读这些,别猜)
**这张表只在 MusePi 仓库 clone 内有效**:它指向的 `docs/` 目录不随发行包分发。在仓库外开发扩展时改用运行时可观测的事实——`settings.schema` 的字段、`extension_status` 检视面、`~/.musepi/agent/` 下的实际文件、`--help`——不要按这些路径去找文件,也不要声称读过读不到的文件。
| 任务 | 文档 |
|---|---|
| 扩展 API 全貌/事件名/命令上下文 | `docs/extensions.md` |
| 插件配置(manifest config / pi.config / 资源卡) | `docs/extensions-dev.md` §13 |
| 加载机制细节(发现顺序/路径解析/禁用) | `docs/extension-loading.md` |
| 自定义工具(模型直接调用) | `docs/custom-tools.md` |
| Hook(pre/post 脚本、可突变什么) | `docs/hooks.md` |
| 加 LLM provider(如新模型厂) | `docs/adding-a-provider.md` + 参照 `packages/ai/src/registry/` 现有 provider 的写法(一个 def 文件 + 一个 registry 行 + catalog 条目) |
| MCP server/工具 | `docs/mcp-config.md` / `docs/mcp-server-tool-authoring.md` |
| 插件市场/安装器 | `docs/marketplace.md` / `docs/plugin-manager-installer-plumbing.md` |
| 设置项(settings.schema) | `docs/settings.md` + `src/config/settings-schema.ts`(单文件权威) |
| 斜杠命令实现 | `docs/slash-command-internals.md` |
| 系统提示词/技能注入 | `docs/context-files.md` / `docs/skills.md` |
| GUI 侧(daemon RPC/Electron) | `docs/gui-implementation.md`(daemon 契约/IPC 形状) |
## 硬约束
- **类型**:扩展 API 类型从 `@musepi/pi-coding-agent` 导入(`ExtensionAPI`/`CustomToolAPI`);不要用 `any` 绕过
- **ESM**:无 `require`;`verbatimModuleSyntax` 下类型导入用 `import type`
- **Zod**:工具参数 schema 用 `pi.zod`(扩展工厂参数)——不用 typebox(扩展侧)
- **错误**:工具执行抛错会被包装成工具错误——用有意义的 message
- **权限**:扩展与 agent 同权限(非沙箱)——破坏性操作需用户确认(工具审批链)
- **测试**:扩展逻辑抽纯函数可单测(`bun test`);端到端用真实会话跑一次工具调用验证