KOReader 插件开发
KOReader 的前端是纯 Lua (LuaJIT),运行在一个小型小部件工具包上。插件是一个名为 <名称>.koplugin/ 的目录,其中包含 main.lua(代码)和 _meta.lua(显示信息)。没有构建步骤,因此最快的方法是遵循以下经过验证的约定,并使用捆绑脚本进行验证,而不是猜测 API。
官方文档:https://koreader.rocks/doc/(API 参考,从源代码生成)以及 https://github.com/koreader/koreader 上的源代码(plugins/ 包含约 40 个实际插件;plugins/hello.koplugin 是最简化的插件)。当不确定签名时,请查看模块的文档页面或阅读源代码——KOReader API 在不同版本之间会发生变化,文档站点是比记忆更权威的来源。
工作流程
- 脚手架(新插件):
python3 <skill>/scripts/new_plugin.py <id> "<全名>" [--dir <路径>]从模板创建<id>.koplugin/,包含菜单项、Dispatcher 动作、设置持久化和生命周期钩子。在此基础上编辑,而不是从头编写。 - 实现:使用此文件中的模式;仅当需要时才打开参考文件:
references/plugin-anatomy.md——文件布局、加载器规则、is_doc_only、初始化顺序、菜单sorting_hint值references/ui-widgets.md——InfoMessage、ConfirmBox、InputDialog、MultiInputDialog、Menu、ButtonDialog、SpinWidget、Notification,附带可工作代码片段references/events-and-lifecycle.md——onXxx处理程序、广播、调度、Dispatcher、阅读器与文件管理器上下文references/settings-and-storage.md——LuaSettings、G_reader_settings、每本书的文档设置、DataStorage 路径references/testing-and-verification.md——模拟器(./kodev run)、luacheck、busted 规范、设备安装和日志读取references/pitfalls.md——导致 KOReader 崩溃或静默无反应的错误;完成前通读一遍
- 验证(始终在确认生效之前进行):
python3 <skill>/scripts/validate_plugin.py <路径/到/x.koplugin>。它会检查结构、_meta.lua、菜单连接、gettext 使用、LuaJIT 语法(luajit -bl)以及存在这些工具时的luacheck。修复每个 ERROR;阅读每个 WARN。然后诚实地说出哪些内容已验证,哪些未验证——验证器无法证明运行时行为;只有模拟器或设备才能做到(请参阅测试参考)。
最小可行插件
-- _meta.lua
local _ = require("gettext")
return {
fullname = _("我的插件"),
description = _([[在插件管理中显示的一句话。]]),
}
-- main.lua
local Dispatcher = require("dispatcher")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local _ = require("gettext")
local MyPlugin = WidgetContainer:extend{
name = "myplugin", -- 唯一ID,与目录前缀匹配
is_doc_only = false, -- true = 仅在打开书籍时加载
}
function MyPlugin:onDispatcherRegisterActions()
Dispatcher:registerAction("myplugin_hello", {
category = "none", event = "MyPluginHello",
title = _("我的插件:你好"), general = true,
})
end
function MyPlugin:init()
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
end
function MyPlugin:addToMainMenu(menu_items)
menu_items.myplugin = {
text = _("我的插件"),
sorting_hint = "more_tools",
callback = function() self:onMyPluginHello() end,
}
end
function MyPlugin:onMyPluginHello()
UIManager:show(InfoMessage:new{ text = _("来自我的插件的问候") })
return true -- 事件已消费
end
return MyPlugin
重要的规则(及原因)
- 将所有用户可见的字符串包裹在
_()中——KOReader 通过 gettext 进行翻译;未包裹的字符串永远无法本地化。使用T = require("ffi/util").template进行插值(T(_("第 %1 页"), n)),而不是..连接,因为不同语言的词序不同。 - 事件是名为
on<事件名称>的方法;只有当你消费了事件时才返回true,否则其他小部件(包括核心 KOReader)将停止接收该事件。 - 切勿阻塞 UI 线程。 网络调用、大文件扫描和
os.execute会冻结单线程的电纸书设备。对于长时间工作,请使用NetworkMgr:runWhenOnline、UIManager:scheduleIn、Trapper:wrap(子进程+进度条)。 - 使用
LuaSettings持久化,并有意刷新。 设置文件位于DataStorage:getSettingsDir()中;在更改后(或在onFlushSettings中)调用:flush()——否则崩溃或电池耗尽会导致数据丢失。 - 清理 定时器(
UIManager:unschedule(fn))和在onCloseWidget/onCloseDocument中打开的部件;泄漏的调度回调会持续唤醒设备并消耗电池。 - 电纸书感知的 UI:优先使用带
timeout的InfoMessage,避免动画,保持刷新最少(仅在更改的部件上使用UIManager:setDirty(widget, "ui"))。 - 使用
logger(require("logger");logger.dbg/info/warn/err)而不是print;输出会发送到模拟器中的 stdout 和设备上的crash.log。 - 保护可选依赖项(
self.ui.statistics、self.ui.document、Device:hasFrontlight()):同一个插件可能会在文件管理器或没有该功能的设备上加载。
反馈
说明你创建/更改的内容、准确的验证器命令及其结果,以及什么内容仍未验证(例如“未在模拟器中运行”)。如果验证器或 luacheck 不可用,请说明,而不是暗示检测通过。