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 插件分类,用于商店展示。可选值:fileimagedevelopmentsystemother(默认)
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
安装插件 onLoadonInstall
更新插件 旧插件 onUnload → 新插件 onLoadonUpdate(oldVersion)
卸载插件 onUninstallonUnload
禁用/启用 onUnload / onLoad

注意onInstallonUpdate 在插件环境完全就绪后才触发,可安全使用所有 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 语法
菜单不出现 检查匹配规则(showOnFilesfileMatchType 等)
点击无响应 确认 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

下一步

Developer Documentation
User Guide
Getting Started Script Menus FAQ
Script Development
Development Guide
Plugin Development
Quick Start Development Guide Example Plugins
API Reference
Overview API Query Plugin Info Logging Finder Context Plugin Settings Internationalization
UI & Interaction
Dialog Progress Notification Chooser WebView Status Bar Dock
Files & Paths
File Operations Path Utilities Finder Actions Trash Extended Attributes Metadata File Watcher
Data Formats
JSON Plist CSV XML PDF Image
Text & Encoding
String Regex Date & Time Color Crypto
System
Shell Commands Process Application System Info AppleScript Shortcuts
System Info
Network Power/Battery Screen/Appearance Audio Bluetooth Location
Network
HTTP WebSocket URL
Input & Clipboard
Keyboard Mouse Hotkey Clipboard Window
Storage
SQLite Keychain UserDefaults
Media
OCR QR Code
Utilities
Archive UTI Share Timer Wake Lock Thread