iRightMenu Pro 插件开发指南
本指南涵盖了开发 iRightMenu Pro 插件所需的全部知识。
目录
插件结构
每个插件由两个核心文件组成:
- plugin.json - 声明元数据、菜单、设置(纯配置,无代码)
- main.lua - 实现菜单处理函数(业务逻辑)
配置文件 (plugin.json)
完整格式
{
"manifestVersion": 1,
"id": "com.example.my-plugin",
"name": "我的插件",
"version": "1.0.0",
"minEngineVersion": "2.0.0",
"author": "作者名",
"description": "插件描述",
"main": "main.lua",
"icon": "icon.png",
"locale": "zh-Hans",
"category": "file",
"type": "context",
"permissions": ["file", "http"],
"settings": {
"title": "插件设置",
"fields": [...]
},
"menus": [...]
}
基础字段
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
manifestVersion |
Int | 否 | plugin.json 格式规范版本(当前为 1) |
id |
String | 是 | 唯一标识符(反向域名格式) |
name |
String | 是 | 显示名称(支持 i18n 占位符) |
version |
String | 是 | 语义化版本(如 “1.0.0”) |
minEngineVersion |
String | 否 | 最低 iRightMenu 版本要求 |
author |
String | 否 | 作者名称 |
description |
String | 否 | 插件描述 |
main |
String | 否 | 主脚本文件名(默认 “main.lua”) |
icon |
String | 否 | 图标文件名(相对路径) |
locale |
String | 否 | 默认语言(如 “en”、”zh-Hans”) |
homepage |
String | 否 | 插件主页 URL(官网、源码仓库等) |
category |
String | 否 | 插件分类,用于商店展示。可选值:file、image、development、system、other(默认) |
type |
String | 否 | 插件类型:"context"(右键菜单,默认)或 "resident"(常驻插件) |
permissions |
[String] | 否 | 权限声明(省略=全部允许,[]=仅安全 API,["*"]=全部) |
权限系统
插件通过 permissions 字段声明所需 API 权限。权限分为 4 个等级:
L0 安全(Safe)— 无需声明,始终可用
| 权限名 | API 模块 | 说明 |
|---|---|---|
log |
app.log |
日志记录 |
context |
app.context |
Finder 上下文 |
dialog |
app.dialog |
对话框和表单 |
progress |
app.progress |
进度对话框 |
path |
app.path |
路径工具 |
i18n |
app.i18n |
国际化 |
json |
app.json |
JSON 操作 |
string |
app.string |
字符串工具 |
date |
app.date |
日期时间 |
regex |
app.regex |
正则表达式 |
notification |
app.notification |
系统通知 |
settings |
app.settings |
插件设置 |
thread |
app.thread |
并发/协程 |
uti |
app.uti |
类型标识符 |
url |
app.url |
URL 解析构建 |
csv |
app.csv |
CSV 解析生成 |
xml |
app.xml |
XML 解析生成 |
color |
app.color |
颜色工具 |
L1 标准(Standard)— 需在 permissions 中声明
| 权限名 | API 模块 | 说明 |
|---|---|---|
file |
app.file |
文件读写 |
http |
app.http |
HTTP 请求 |
archive |
app.archive |
归档操作 |
plist |
app.plist |
Plist 读写 |
defaults |
app.defaults |
UserDefaults 偏好 |
image |
app.image |
图片处理 |
crypto |
app.crypto |
加密编码 |
clipboard |
app.clipboard |
剪贴板 |
finder |
app.finder |
Finder 操作 |
system |
app.system |
系统信息 |
keychain |
app.keychain |
安全凭证存储 |
xattr |
app.xattr |
扩展属性/标签 |
trash |
app.trash |
废纸篓 |
metadata |
app.metadata |
Spotlight 元数据 |
pdf |
app.pdf |
PDF 操作 |
network |
app.network |
网络信息 |
power |
app.power |
电源/电池 |
screen |
app.screen |
屏幕/外观 |
audio |
app.audio |
音频控制 |
ocr |
app.ocr |
文字识别 |
qrcode |
app.qrcode |
二维码 |
wakelock |
app.wakelock |
防休眠 |
watcher |
app.watcher |
文件监听 |
share |
app.share |
macOS 分享 |
shortcuts |
app.shortcuts |
快捷指令 |
timer |
app.timer |
定时器 |
websocket |
app.websocket |
WebSocket 客户端 |
chooser |
app.chooser |
快速选择器 |
dock |
app.dock |
Dock 角标/弹跳 |
L2 敏感(Sensitive)— 安装时显示权限警告
| 权限名 | API 模块 | 说明 |
|---|---|---|
shell |
app.shell |
Shell 命令执行 |
database |
app.database |
SQLite 数据库 |
process |
app.process |
进程管理 |
application |
app.application |
应用程序控制 |
webview |
app.webview |
WebView 窗口 |
statusbar |
app.statusbar |
状态栏图标 |
L3 危险(Dangerous)— 安装时显示强烈安全警告
| 权限名 | API 模块 | 说明 |
|---|---|---|
applescript |
app.applescript |
AppleScript(完整系统访问) |
keyboard |
app.keyboard |
键盘模拟(需辅助功能权限) |
mouse |
app.mouse |
鼠标模拟(需辅助功能权限) |
hotkey |
app.hotkey |
全局热键(需辅助功能权限) |
权限声明示例
// 只使用安全 API(无需声明)
{"permissions": []}
// 需要文件和网络
{"permissions": ["file", "http"]}
// 需要全部权限
{"permissions": ["*"]}
// 省略 permissions 字段 = 允许全部(兼容旧插件)
{}
菜单配置
{
"menus": [
{
"id": "menu_action",
"title": "执行操作",
"description": "菜单项描述",
"icon": "action.png",
"enabled": true,
"showInStatusBar": false,
"primaryMenu": false,
"dynamic": false,
"showOnFiles": true,
"showOnFolders": false,
"fileMatchType": "none",
"fileMatchPatterns": [],
"fileExecutableOnly": false,
"minFiles": 1,
"maxFiles": 0,
"folderMatchType": "none",
"folderMatchPatterns": [],
"minFolders": 0,
"maxFolders": 0,
"notifyType": "none",
"requireConfirm": false,
"playSound": false,
"soundPath": null
}
]
}
菜单字段
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id |
String | 必需 | 菜单项唯一标识,用于绑定点击回调和动态行为 |
title |
String | 必需 | 菜单项文本(支持 i18n) |
description |
String | ”” | 工具提示/描述 |
icon |
String | null | 菜单图标文件名 |
enabled |
Boolean | true | 是否启用 |
showInContextMenu |
Boolean | true | 在右键菜单中显示 |
showInStatusBar |
Boolean | false | 在状态栏菜单中显示 |
primaryMenu |
Boolean | false | 在主菜单中显示(非子菜单) |
dynamic |
Boolean | false | 启用动态菜单(支持动态标题和动态可见性) |
文件匹配规则
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showOnFiles |
Boolean | true | 选中文件时显示 |
fileMatchType |
String | “none” | 文件匹配模式 |
fileMatchPatterns |
Array | [] | 扩展名/正则匹配模式 |
fileExecutableOnly |
Boolean | false | 仅匹配可执行文件 |
minFiles |
Number | 0 | 最少选中文件数(0=不限) |
maxFiles |
Number | 0 | 最多选中文件数(0=不限) |
文件夹匹配规则
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showOnFolders |
Boolean | true | 选中文件夹时显示 |
folderMatchType |
String | “none” | 文件夹匹配模式(仅支持 “none”、”regex”) |
folderMatchPatterns |
Array | [] | 正则匹配模式 |
minFolders |
Number | 0 | 最少选中文件夹数(0=不限) |
maxFolders |
Number | 0 | 最多选中文件夹数(0=不限) |
执行配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
notifyType |
String | “none” | 执行后通知方式:"none"(不通知)、"notification"(系统通知)。插件默认 none,插件自行通过 app.notification API 管理反馈 |
requireConfirm |
Boolean | false | 执行前是否弹出确认对话框 |
playSound |
Boolean | false | 执行后是否播放提示音 |
soundPath |
String | null | 提示音文件名(如 "Glass.aiff"),为空时使用系统默认提示音 |
文件匹配类型
| 类型 | 说明 | fileMatchPatterns |
|---|---|---|
none |
通用匹配,不限制文件类型 | 忽略 |
extension |
按文件扩展名匹配 | ["jpg", "png", "gif"] |
regex |
按正则表达式匹配文件名 | ["^IMG_\\d+"] |
executable |
仅匹配可执行文件 | 忽略 |
脚本文件 (main.lua)
基本结构
-- 继承 Plugin 基类创建插件类
local MyPlugin = Plugin:extend()
-- 初始化:注册处理函数
function MyPlugin:init()
-- 注册菜单动作处理函数
self:registerHandler("menu_id", self.handleAction)
-- 注册动态检查函数(可选)
self:registerVisibility("menu_id", self.shouldShow)
app.log.info("插件已初始化")
end
-- 菜单动作处理函数
-- @param context {selectedFiles, currentDirectory}
function MyPlugin:handleAction(context)
local files = context.selectedFiles
local dir = context.currentDirectory
-- 你的业务逻辑...
end
-- 动态可见性检查(可选)
-- @param context {selectedFiles, currentDirectory}
-- @return boolean
function MyPlugin:shouldShow(context)
return #context.selectedFiles > 0
end
-- 必须返回插件类(引擎自动实例化)
return MyPlugin
context 上下文对象
传递给处理函数的 context 对象包含:
context = {
selectedFiles = {"/path/to/file1", "/path/to/file2", ...},
currentDirectory = "/path/to/current/dir"
}
Plugin 基类
Plugin 基类已预加载到全局环境中。
创建插件
-- 继承基类
local MyPlugin = Plugin:extend()
-- 实现 init()
function MyPlugin:init()
-- 在这里注册处理函数
end
-- 返回插件类(必需,引擎自动实例化)
return MyPlugin
元数据方法
| 方法 | 返回值 | 说明 |
|---|---|---|
self:getId() |
String | 插件 ID |
self:getName() |
String | 插件名称 |
self:getVersion() |
String | 插件版本 |
self:getAuthor() |
String | 插件作者 |
self:getDescription() |
String | 插件描述 |
self:getMetadata() |
Table | 完整元数据 |
self:getInstallPath() |
String | 插件安装目录路径 |
处理函数注册
-- 注册菜单动作处理函数
self:registerHandler("menu_id", self.handleAction)
-- 注册动态可见性检查
self:registerVisibility("menu_id", self.shouldShow)
重要:使用方法引用(
self.methodName)而非闭包。 处理函数接收(self, context)两个参数,使用闭包会导致参数错位。
生命周期方法(可选)
function MyPlugin:onLoad()
-- 插件环境加载完成后调用(每次启动)
end
function MyPlugin:onUnload()
-- 插件环境即将卸载时调用
end
function MyPlugin:onInstall()
-- 首次安装后调用(在 onLoad 之后触发)
end
function MyPlugin:onUpdate(oldVersion)
-- 插件更新后调用(在 onLoad 之后触发)
-- oldVersion: 更新前的版本号
app.log.info("Updated from " .. oldVersion)
end
function MyPlugin:onUninstall()
-- 卸载前调用(在 onUnload 之前触发,可用于清理资源)
end
调用时序:
| 场景 | 调用顺序 |
|---|---|
| 应用启动 | onLoad |
| 安装插件 | onLoad → onInstall |
| 更新插件 | 旧插件 onUnload → 新插件 onLoad → onUpdate(oldVersion) |
| 卸载插件 | onUninstall → onUnload |
| 禁用/启用 | onUnload / onLoad |
注意:
onInstall和onUpdate在插件环境完全就绪后才触发,可安全使用所有 API。onUninstall在文件删除前同步调用,适合清理 Keychain 凭证、临时文件等资源。
访问插件资源
function MyPlugin:init()
-- 获取内置工具路径
local tool = self:getInstallPath() .. "/bin/mytool"
-- 使用 app.path.join 拼接路径
local config = app.path.join(
self:getInstallPath(),
"config",
"default.json"
)
-- 检查资源是否存在
if app.path.exists(self:getInstallPath() .. "/resources/data.json") then
app.log.info("资源文件存在")
end
end
菜单匹配规则
基本匹配
{
"menus": [{
"id": "process",
"title": "处理",
"showOnFiles": true,
"showOnFolders": false,
"minFiles": 1
}]
}
此菜单在以下情况显示:
- 至少选中 1 个文件
- 未选中文件夹
扩展名匹配
{
"menus": [{
"id": "resize_image",
"title": "调整图片大小",
"fileMatchType": "extension",
"fileMatchPatterns": ["jpg", "jpeg", "png", "gif", "webp"],
"showOnFiles": true,
"showOnFolders": false
}]
}
此菜单仅对图片文件显示。
正则匹配
{
"menus": [{
"id": "process_screenshots",
"title": "处理截图",
"fileMatchType": "regex",
"fileMatchPatterns": ["^Screenshot.*\\.png$", "^截屏.*\\.png$"],
"showOnFiles": true
}]
}
动态检查
对于复杂条件,使用 dynamic:
{
"menus": [{
"id": "git_commit",
"title": "Git 提交",
"dynamic": true
}]
}
function MyPlugin:init()
self:registerHandler("git_commit", self.doCommit)
self:registerVisibility("git_commit", self.canCommit)
end
function MyPlugin:canCommit(context)
-- 检查当前目录是否是 git 仓库
local result = app.shell.execute("git -C '" .. context.currentDirectory .. "' rev-parse 2>/dev/null")
return result.code == 0
end
插件设置
定义设置
{
"settings": {
"title": "我的插件设置",
"fields": [
{
"key": "api_key",
"type": "text",
"label": "API 密钥",
"default": "",
"placeholder": "请输入你的 API 密钥"
},
{
"key": "quality",
"type": "number",
"label": "质量",
"default": 80
},
{
"key": "auto_process",
"type": "checkbox",
"label": "自动处理",
"default": true
},
{
"key": "output_format",
"type": "select",
"label": "输出格式",
"default": "png",
"options": ["png", "jpg", "webp"]
},
{
"key": "output_dir",
"type": "folder",
"label": "输出目录",
"default": ""
}
]
}
}
字段类型
| 类型 | 说明 | 返回值类型 |
|---|---|---|
text |
单行文本输入 | String |
number |
数字输入 | Number |
checkbox |
复选框 | Boolean |
select |
下拉菜单 | String |
folder |
文件夹选择器 | String(路径) |
file |
文件选择器 | String(路径) |
在 Lua 中使用设置
-- 获取设置值(带默认值)
local apiKey = app.settings.get("api_key", "")
local quality = app.settings.get("quality", 80)
-- 设置值
app.settings.set("last_used", os.time())
-- 获取所有设置
local all = app.settings.getAll()
-- 删除设置
app.settings.delete("temporary_value")
国际化
目录结构
com.example.my-plugin/
├── plugin.json
├── main.lua
└── locales/
├── en.json
└── zh-Hans.json
在 plugin.json 中使用占位符
{
"name": "{{plugin.name}}",
"description": "{{plugin.description}}",
"menus": [
{
"id": "action",
"title": "{{menu.action}}"
}
],
"settings": {
"title": "{{settings.title}}",
"fields": [
{
"key": "option",
"type": "text",
"label": "{{settings.option}}"
}
]
}
}
语言文件
locales/en.json(平铺 dot-notation 格式)
{
"plugin.name": "My Plugin",
"plugin.description": "A useful plugin",
"menu.action": "Do Something",
"settings.title": "Settings",
"settings.option": "Option Label",
"messages.success": "Processed %d files successfully",
"messages.error": "An error occurred: %s"
}
locales/zh-Hans.json
{
"plugin.name": "我的插件",
"plugin.description": "一个实用的插件",
"menu.action": "执行操作",
"settings.title": "设置",
"settings.option": "选项标签",
"messages.success": "成功处理了 %d 个文件",
"messages.error": "发生错误:%s"
}
在 Lua 中使用
-- 获取翻译字符串
local title = app.i18n.t("plugin.name")
-- 带格式参数
local msg = app.i18n.t("messages.success", 5)
-- 英文:"Processed 5 files successfully"
-- 中文:"成功处理了 5 个文件"
-- 带默认值
local text = app.i18n.t("unknown.key", nil, "默认文本")
-- 检查 key 是否存在
if app.i18n.has("messages.custom") then
-- ...
end
-- 当前语言
local locale = app.i18n.locale() -- "en" 或 "zh-Hans"
-- 可用语言列表
local locales = app.i18n.locales() -- {"en", "zh-Hans"}
调试技巧
日志
-- 日志级别(从低到高)
app.log.verbose("详细信息") -- 仅插件日志
app.log.debug("调试信息") -- 仅插件日志
app.log.info("普通信息") -- 仅插件日志
app.log.warning("警告") -- 插件 + 系统日志
app.log.error("错误") -- 插件 + 系统日志
-- 获取日志文件路径
local logPath = app.log.getPath()
-- 读取最后 N 行
local content = app.log.read(100)
-- 清空日志文件
app.log.clear()
日志位置
- 插件日志:
~/Library/Group Containers/group.net.ymlab.iRightMenu.pro/Logs/Plugins/{plugin_id}.log - 系统日志:
~/Library/Group Containers/group.net.ymlab.iRightMenu.pro/Logs/iRightMenu.log
实时查看日志
# 查看所有日志
tail -f ~/Library/Group\ Containers/group.net.ymlab.iRightMenu.pro/Logs/*.log
# 查看特定插件
tail -f ~/Library/Group\ Containers/group.net.ymlab.iRightMenu.pro/Logs/Plugins/com.example.myplugin.log
# 过滤错误
tail -f ~/Library/Group\ Containers/group.net.ymlab.iRightMenu.pro/Logs/*.log | grep -i error
常见问题
| 问题 | 解决方案 |
|---|---|
| 插件不显示 | 检查 plugin.json JSON 语法 |
| 菜单不出现 | 检查匹配规则(showOnFiles、fileMatchType 等) |
| 点击无响应 | 确认 registerHandler 的 ID 与菜单 ID 匹配 |
| API 返回 nil | 查看日志获取详细错误信息 |
| 设置未保存 | 确保使用正确的 key 名称 |
调试模式
开发时启用详细日志:
function MyPlugin:init()
app.log.verbose("=== 插件初始化开始 ===")
app.log.verbose("安装路径: " .. self:getInstallPath())
app.log.verbose("版本: " .. self:getVersion())
-- ... 注册处理函数 ...
app.log.verbose("=== 插件初始化完成 ===")
end
最佳实践
1. 使用唯一的插件 ID
com.yourcompany.plugin-name
2. 优雅地处理错误
function MyPlugin:processFiles(context)
local success, err = pcall(function()
for _, file in ipairs(context.selectedFiles) do
self:processFile(file)
end
end)
if not success then
app.log.error("处理失败: " .. tostring(err))
app.dialog.alert("错误", "文件处理失败")
end
end
3. 提供用户反馈
function MyPlugin:longOperation(context)
-- 长时间操作显示进度
app.progress.show("处理中", {
message = "准备中...",
indeterminate = false
})
local files = context.selectedFiles
for i, file in ipairs(files) do
app.progress.update(
(i / #files) * 100,
"正在处理: " .. app.path.basename(file)
)
self:processFile(file)
end
app.progress.hide()
-- 完成时发送通知
app.notification.show("完成", "已处理 " .. #files .. " 个文件")
end
4. 始终提供默认值
local quality = app.settings.get("quality", 80) -- 默认值:80
local format = app.settings.get("format", "png") -- 默认值:"png"
5. 记录重要操作
function MyPlugin:deleteFiles(context)
app.log.info("[MyPlugin] 开始删除操作")
app.log.info("[MyPlugin] 待删除文件数: " .. #context.selectedFiles)
for _, file in ipairs(context.selectedFiles) do
app.log.info("[MyPlugin] 删除: " .. file)
-- ...
end
app.log.info("[MyPlugin] 删除操作完成")
end
6. 支持国际化
即使最初只支持一种语言,也使用 i18n 占位符,方便将来添加翻译。
7. 验证用户输入
function MyPlugin:renameFiles(context)
local newName = app.dialog.input("输入新名称", "")
if not newName or newName == "" then
return -- 用户取消或输入为空
end
-- 验证文件名
if newName:match('[/\\:*?"<>|]') then
app.dialog.alert("无效名称", "文件名包含非法字符")
return
end
-- 继续重命名...
end
8. 清理资源
function MyPlugin:onUnload()
-- 关闭数据库连接
if self.db then
app.database.close(self.db)
self.db = nil
end
-- 取消待处理的操作
thread.stopAll()
end