工具插件开发
工具插件(type: "tool",transport: "in-process")的代码运行在客户端主进程内,可以访问宿主原生能力,是最灵活也最常用的一类插件。
适用场景
| 场景 | 是否推荐 in-process |
|---|---|
| 纯计算/文本处理工具 | ✅ 启动快、无依赖 |
| 需要读写本地文件、访问工作区 | ✅ 可用 getWorkspacePath |
| 需要截图、OCR、图像处理 | ✅ 可用 sharp / ocr 宿主能力 |
| 需要模拟鼠标键盘、控制桌面 | ✅ 可用 getDesktopControlManager |
| 需要注册定时任务 | ✅ 可用 cronScheduler |
| 需要向客户端注入 UI | ✅ 必须 in-process |
| 依赖大量第三方 npm 包 | ⚠️ 需自行打包依赖,或改用 stdio |
| 需要崩溃隔离 | ❌ 用 stdio |
目录结构
plugins/my-tool/
├── manifest.json # 必需
├── index.js # 必需:MCP 工厂入口
├── package.json # 声明 "type": "module"
├── ui-src/ # 可选:UI 扩展源码
├── ui.js # 可选:UI 构建产物
└── icon/icon.png # 可选:本地图标manifest.json
json
{
"id": "my-tool",
"name": "My Tool",
"nameZh": "我的工具",
"description": "Do something useful",
"descriptionZh": "做一些有用的事",
"type": "tool",
"version": "1.0.0",
"main": "index.js",
"icon": "Sparkles",
"category": "utility",
"capabilities": {
"mcp": {
"transport": "in-process",
"autoConnect": true
}
},
"configSchema": { "fields": [] },
"permissions": [],
"platforms": ["darwin", "win32", "linux"],
"license": "MIT"
}入口代码结构
推荐固定三段式:获取 Host → 定义辅助函数 → 导出工厂。
javascript
/**
* My Tool - 插件入口
*/
// ① 获取宿主注入的 MCP 运行时
function getHost() {
const host = globalThis.__AWEECLAW_HOST__
if (!host) throw new Error('Host services not available. globalThis.__AWEECLAW_HOST__ is not set.')
return host
}
const { McpServer, InMemoryTransport, z } = getHost()
// ② 辅助函数
function text(t) {
return { type: 'text', text: String(t) }
}
function ok(...items) {
return { content: items }
}
function err(message) {
return { content: [text(message)], isError: true }
}
// ③ 导出工厂,返回 { server, clientTransport }
export function createMcpServer() {
const server = new McpServer(
{ name: 'my-tool', version: '1.0.0' },
{
capabilities: { tools: { listChanged: false } },
instructions: 'One-line summary of what this plugin does.',
},
)
server.tool(
'hello',
'Say hello to someone',
{
name: z.string().default('world').describe('Name to greet'),
loud: z.boolean().default(false).describe('Uppercase the greeting'),
},
async (args) => {
try {
const greeting = `Hello, ${args.name}!`
return ok(text(args.loud ? greeting.toUpperCase() : greeting))
} catch (e) {
return err(`Failed: ${e.message}`)
}
},
)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
server.connect(serverTransport).catch(() => {})
return { server, clientTransport }
}工具注册签名
javascript
server.tool(
'tool_name', // 工具名:小写下划线,智能体按此调用
'One-line description', // 描述:LLM 据此判断何时调用,必须写清"做什么/何时用"
{ /* zod schema */ }, // 参数定义
async (args, extra) => ({ content: [...] }), // 处理器
)参数用 zod 描述
| 写法 | 作用 |
|---|---|
z.string() / z.number() / z.boolean() | 基础类型 |
z.string().min(1).max(200) | 长度约束(同时约束 LLM 输出) |
z.number().int().min(1).max(1000) | 整数与范围 |
z.array(z.string()) | 数组 |
z.enum(['a', 'b']) | 枚举 |
.default(...) | 默认值,缺省时 LLM 可省略该参数 |
.optional() | 可选参数 |
.describe('...') | 参数级说明,强烈建议每个参数都写 |
返回值约定
javascript
// 成功
return { content: [{ type: 'text', text: '结果文本' }] }
// 失败:不要抛异常,返回 isError
return { content: [{ type: 'text', text: '失败原因' }], isError: true }抛出的异常会被客户端捕获并转成通用错误信息,智能体拿不到具体原因,因此业务错误一律用 isError 返回。
访问宿主能力
in-process 插件可通过 globalThis.__AWEECLAW_HOST__ 访问客户端原生能力。
| 字段 | 说明 |
|---|---|
McpServer / InMemoryTransport / z | MCP 运行时与 zod |
logger | 分区日志器(logger.system.info(...)) |
getWorkspacePath() | 当前工作区路径,返回 null 时应降级到临时目录 |
sharp | 图像处理(缩放、格式转换、裁剪) |
ocr / macVisionOcrRouter | 文字识别 |
nativeImage | Electron 原生图像对象 |
getDesktopControlManager | 桌面控制(鼠标、键盘、窗口、截图) |
inputListener | 截图流输入监听(供录制类插件使用) |
cronScheduler | 注册插件级定时任务 |
pptxgen / pptPreview / parsePptxFile | PPT 生成、实时预览、解析 |
sharp | 见上 |
perceptionStore / localEmbedder / vlmModelManager | 感知数据存储、本地嵌入、视觉语言模型 |
behaviorPredictor / codeDependencyGraph / impactAnalyzer | 行为预测、代码依赖图、影响分析 |
monitoringService / iotBridge | 系统监控、IoT 设备 |
示例 —— 把结果写入工作区:
javascript
const { logger, getWorkspacePath } = getHost()
server.tool('export_report', 'Export report to workspace', {}, async () => {
const root = getWorkspacePath()
if (!root) return err('No workspace is open.')
const dir = path.join(root, '.aweeclaw')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(path.join(dir, 'report.md'), '# Report\n', 'utf8')
logger.system.info('[my-tool] report exported to', dir)
return ok(text(`Exported to ${dir}/report.md`))
})权限与 ctx.host
上面的 globalThis.__AWEECLAW_HOST__ 是全局共享对象,不做权限校验(为兼容早期插件保留)。若插件通过原生 PluginRuntime 上下文初始化,应优先使用 ctx.host —— 它会按 manifest.permissions 校验,未声明的能力访问会抛 PermissionDeniedError:
typescript
const host = ctx.host as HostServices // 受限代理
host.ocr.recognize(buffer) // 需声明 desktop.screen 或对应权限规则:
- 用哪个能力,就在
manifest.permissions里声明哪个权限; - 声明了
contributes(UI 扩展)必须加ui.render; - 权限只做能力访问控制,不做网络域名白名单。
注册定时任务
插件可向 cronScheduler 注册自己的定时回调,独立于平台的自动化任务系统(触发时不会进入 Agent 对话流):
javascript
const { cronScheduler } = getHost()
// initialize 阶段注册
cronScheduler.register({
pluginId: 'my-tool',
name: 'hourly-sync',
pattern: '0 * * * *',
handler: async () => {
logger.system.info('[my-tool] hourly sync triggered')
},
})
// 卸载时批量清理
cronScheduler.unregisterByPlugin('my-tool')原生插件运行时(进阶)
若插件不通过 MCP 暴露工具,而是实现客户端原生插件接口(如渠道、Provider、Hook 类插件),则改用 PluginRuntime:
typescript
import type { PluginRuntime, PluginContext } from '@shared/plugin-sdk'
export const runtime: PluginRuntime = {
async initialize(ctx: PluginContext) {
ctx.logger.info('plugin initialized', ctx.getConfig())
const token = await ctx.getSecret('API_KEY')
ctx.registerHook('before-tool-call', (payload) => ({ proceed: true }))
},
async destroy() {
/* 释放资源 */
},
async healthCheck() {
return { healthy: true }
},
}PluginContext 提供的能力:
| 成员 | 说明 |
|---|---|
pluginId | 插件 ID |
dataDir | 插件独立数据目录 |
logger | 分区日志器 |
getConfig() / setConfig() | 读写配置 |
getSecret(key) / setSecret(key, value) | 读写安全存储中的密钥 |
registerHook(event, handler) | 注册生命周期钩子,返回取消函数 |
sendToRenderer(channel, ...args) | 向渲染进程发送 IPC 事件 |
host | 受限的原生能力代理(受 permissions 校验) |
调试
bash
node --check index.js # 语法检查运行时行为在客户端观察:「设置 → MCP 服务」查看连接状态与调用日志。完整方法见本地调试。

