MyBooks Toolbox Core API
给 MyBooks Toolbox 工具(内置工具与外部工具)用的后端 API(self.api.*)和
前端桥接脚本(toolbox-bridge.js)参考文档。脚手架 CLI 见本仓库
README。
1.2.0
CLI 命令 mytool
类型 builtin / tool
同步自 mybooks/mybooks · 见页尾
backend/tool.py 里,工具类继承
BaseTool,运行时可以通过 self.api 访问下面这些方法;工具的
frontend/index.html 里,引入 <script src="/static/toolbox-bridge.js">
后可以用 window.MyBooksToolBridge。两者都由宿主 MyBooks 在运行时注入,
脚手架生成的项目模板里已经各给了一个最小可用的调用示例。
后端:Core API #core-api
挂在 self.api 上,按命名空间划分:calibre / db /
tasks / messages / storage / settings /
utils。所有方法都是同步调用,
工具作者不需要关心背后是 Calibre 的 legacy DB、new_api,
还是应用自己的 SQLAlchemy session——这层解耦正是 Core API 存在的意义,接口本身按
语义化版本演进(manifest.json 的 core_api_version 声明兼容的最低版本)。
self.api.calibre Calibre 书库读写
查询、导入、修改书籍元数据与格式文件。
get_metadata/set_metadata/import_book
里的 Metadata 类型,指的是
mybooks/mybooks 仓库 webserver/toolbox/core_api.py 里的
BookMetadata——一份结构对齐 Calibre Metadata 的类型契约
(typing.Protocol),不是要你直接 import calibre:
运行时拿到手的仍是原生 Calibre 对象,直接读写 .title/.authors/
.tags/.comments/.series/.pubdate/
.identifiers 等属性即可,其余字段/自定义列用 mi.get(field, default)
/ mi.set(field, val) 读写;类型标注需要时
from webserver.toolbox.core_api import BookMetadata。
calibre.search_books(query, max_results=20) → list[dict]
按 Calibre 搜索语法查询书籍,返回 dict 列表(含 id/title/authors/available_formats 等字段)。
| 参数 | 类型 | 说明 |
|---|---|---|
query | str | Calibre 搜索表达式,如 authors:="张三" |
max_results | int | 最多返回条数,默认 20 |
calibre.search_ids(query) → list[int]
同上,但只返回排序后的 book_id 列表,不取详情,适合只需要遍历 id 的场景(比全量取详情快)。
calibre.get_metadata(book_id, get_cover=False, cover_as_data=False) → BookMetadata
返回指定书籍的 Calibre Metadata 对象。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | Calibre 书籍 ID |
get_cover | bool | 是否附带封面(写入磁盘临时文件路径),默认 False |
cover_as_data | bool | 封面以内存字节形式附带(配合 get_cover),默认 False |
calibre.set_metadata(book_id, mi, force_changes=True) → None
写回书籍元数据;mi 是一个 BookMetadata 对象(可以从 get_metadata 拿到后原地修改再传回)。
calibre.get_data_as_dict(ids) → list[dict]
按 book_id 列表批量返回书籍 dict(含 available_formats/title 等字段),比逐个 get_metadata 更省调用次数。
calibre.cover(book_id) → bytes | None
返回书籍封面的原始字节,没有封面时返回 None。
calibre.set_cover(book_id, cover) → None
设置书籍封面。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 ID |
cover | bytes | 封面原始字节 |
calibre.import_book(mi, formats) → int | None
将本地格式文件与给定元数据一并入库,返回新书的 book_id。
| 参数 | 类型 | 说明 |
|---|---|---|
mi | BookMetadata | 要写入的元数据对象 |
formats | list[str] | 本地文件绝对路径列表(如 ["/tmp/x.epub"]) |
calibre.import_file(user_id, file_path, title, authors, *, delete_after_import=True) → int
比 import_book 更高一层:从文件本身读取元数据兜底、清洗标题/作者、创建 Item
采集记录、可选删除源文件,是"下载后入库"这类场景的推荐入口(rare_book_downloader 等工具在用)。
calibre.merge_formats(source_book_id, target_book_id) → list[str]
把 source_book_id 书籍里 target_book_id 尚不具备的格式复制过去,返回新增的格式名列表(大写)。不会删除来源书籍。
calibre.add_format(book_id, fmt, file_path) → None
给书籍添加/替换一个格式文件;file_path 可以是磁盘路径字符串,也可以是已打开的文件对象。
calibre.format_abspath(book_id, fmt) → str | None
返回指定格式文件在磁盘上的绝对路径;文件不存在时返回 None。
calibre.get_custom(book_id, label) → any
读取自定义列(Calibre 里以 # 开头的列,这里传不带 # 的 label)的值。
calibre.set_custom(label, values) → None
批量写入自定义列的值,values 为 {book_id: value} 形式的字典(一次调用可以写多本书)。
calibre.remove_formats(values) → None
批量删除格式文件,values 为 {book_id: [fmt, ...]}。
calibre.set_language(book_id, language) → None
把书籍的语言字段更新为给定语言代码(如 "zh"/"zht"/"en")。
calibre.all_book_ids() → list[int]
返回书库中所有书籍的 ID 列表(已排序)。批量扫描类工具(格式精简、语言检测等)常用这个做遍历入口。
calibre.delete_book(book_id) → None
从 Calibre 书库及应用自己的 Item 表中一并删除指定书籍,不可撤销,请在有明确用户确认的操作里使用。
self.api.db 应用数据库
读写应用自己的 SQLAlchemy 模型(Reader / Item),不暴露裸的 Session 或 ORM 对象——全部返回/接收 plain dict,避免工具持有脱离 session 生命周期的实例。
db.get_item_by_book_id(book_id) → dict | None
返回 {"id", "book_id", "collector_id"};书籍没有采集记录时返回 None。
db.create_item(book_id, collector_id) → dict
创建一条采集记录,返回同上格式的 dict。
db.delete_item_by_book_id(book_id) → None
删除该书籍的采集记录(存在才删,不存在直接忽略)。
db.get_reader(user_id) → dict | None
返回 {"id", "username", "name", "admin"};用户不存在时返回 None。
self.api.tasks 后台任务
耗时操作(下载、格式转换、批量扫描等)应该跑成后台任务,让用户能在任务面板看到进度,而不是让 HTTP 请求一直挂起。
tasks.create_task(progress_data=None) → int
创建一个后台任务,返回 task_id。任务名称取自工具类的 service_item_name 属性;后台任务面板里该工具的 service_type 自动带 "tool:<tool_id>" 前缀,与内置工具区分展示。
tasks.update_progress(task_id, progress, progress_data=None) → None
更新任务进度,progress 为 0–100 的整数。
tasks.complete_task(task_id, error_message=None) → None
标记任务完成;传 error_message 则标记为失败并附带错误描述。
tasks.make_progress_callback(task_id, progress_data_factory=None, outer_callback=None) → callable
构造一个 (progress: int) -> None 的回调函数,消除手写进度回调时重复的样板代码;常见于给下载器/转换器传 callback= 参数。
self.api.messages 站内消息
给用户发一条持久化的站内通知,独立于任务面板(例如任务已经 complete,但还想额外提示一句"已生成新书,可在书库中查看")。
messages.send_message(user_id, msg, status="info") → None
| 参数 | 类型 | 说明 |
|---|---|---|
user_id | int | 接收用户 ID |
msg | str | 消息内容 |
status | str | "info" / "success" / "warning" / "danger",决定前端展示的样式 |
messages.cleanup_messages(user_id, msg_content, days=31) → int
清理该用户内容重复或超过 days 天未读的消息,返回清理条数。send_message 内部已经会做一次同内容去重,一般不需要单独调用。
self.api.storage 工具专属存储
每个工具都有自己独立的数据目录,路径由 tool_id 派生,不同工具之间互不干扰。
storage.get_work_dir(unique_key=None) → str
返回并创建一个工作目录的绝对路径。传 unique_key(如任务参数、URL)时按其哈希生成独立子目录,适合"每次调用一个临时目录"的场景;不传则返回该工具的共享目录。
storage.cleanup_work_dir(work_dir) → None
递归删除工作目录;失败只记录警告,不向上抛异常(避免因为清理失败影响主流程的成功状态)。
storage.get_config() → dict
读取该工具的持久化配置(JSON 文件),文件不存在或解析失败时返回 {},不抛异常。
storage.set_config(data) → None
把 data(dict)写入该工具的持久化配置文件;适合保存 API Key、用户偏好这类"重启也要记住"的设置。写入失败会抛 RuntimeError。
self.api.settings 系统配置只读白名单
MyBooks 的系统配置(数据库连接串、各种第三方 API key/token 等)不能整份透给工具,
所以这里只开放一个白名单:只有显式登记过的 key 才能读到,其余一律返回调用方传入的
default,不会抛异常。
settings.get(key, default=None) → Any
| 参数 | 类型 | 说明 |
|---|---|---|
key | str | 配置项名称,必须在白名单内才读得到真实值 |
default | Any | key 不在白名单内,或 CONF 里没有这一项时返回的兜底值 |
当前白名单(源头见 mybooks/mybooks 仓库 webserver/toolbox/core_api.py 的
SettingsAPI.ALLOWED_KEYS):
| key | 说明 |
|---|---|
auto_fill_meta | 是否自动补全导入书籍的元数据 |
audio_output_folder | 音频类工具的输出目录路径 |
DEFAULT_LANGUAGE | 站点默认语言,可用作工具自行做多语言展示的兜底 |
self.api 直接读 CONF——
外部工具本来就没有这个能力,内置工具这么做也过不了后续的代码审查。
self.api.utils 通用文本/日期处理
纯函数,不依赖工具自身的状态,转发 mybooks/mybooks 仓库 webserver/utils.py
里的同名函数,行为完全一致——书籍导入/整理类工具解析文件名、标准化标题排序、解析各种
格式日期时常用得到,不需要自己再写一份。
utils.strip(s) → str
去除首尾空白,并过滤掉字符串里其余不可打印字符(如 \x00 一类控制字符)。转发 super_strip。
utils.get_title_sort(title) → str
把书名转成用于排序的 ASCII 小写形式(内部走 Calibre 的 unicode-decomposition),转换失败时原样返回 title,不抛异常。
utils.guess_title_author_from_filename(name) → (str, str|None)
从"《书名》作者:某某"这类常见命名习惯的文件名里拆出 (title, author);name 里不含"作者"关键字时,author 返回 None,title 原样返回(去除首尾空白)。
utils.parse_date(date_str) → datetime|None
按常见格式(含中文"年月日",如 2024年5月1日/2024-05-01/2024/05)解析日期字符串,返回带 UTC 时区的 datetime;解析不出来或传空值时返回 None。
self.api 直接碰 Calibre/SQLAlchemy 内部对象——那样会在
MyBooks 升级时随时被破坏,且商店审核也不会通过。
前端:toolbox-bridge.js #bridge
工具前端是一份自包含的静态站点,由宿主 MyBooks 用 <iframe> 加载,不要求用 Vue、
不要求和宿主的前端框架版本一致。在 index.html 里引入:
<script src="/static/toolbox-bridge.js"></script>
之后即可使用全局对象 window.MyBooksToolBridge:
bridge.toolId string
当前工具的 tool_id,从页面自身 URL(/get/tool/{tool_id}/index.html)解析得到。
bridge.theme "light" | "dark"(getter,始终最新)
宿主当前主题。宿主切换深浅色主题时会实时更新,不需要刷新页面。详见下方 主题 / 语言联动。
bridge.locale string(getter,始终最新)
宿主当前语言(如 "zh" / "zh-TW" / "en")。宿主切换语言时会实时更新,不需要刷新页面。详见下方 主题 / 语言联动。
bridge.onThemeChange(fn) → 取消订阅函数
注册一个 fn(newTheme, oldTheme) 回调,宿主切换主题时调用;不调用也没关系——
bridge.theme 本身随时读到的都是最新值,只是不会主动收到"变了"这件事的推送。
bridge.onLocaleChange(fn) → 取消订阅函数
注册一个 fn(newLocale, oldLocale) 回调,宿主切换语言时调用;用法与
onThemeChange 对称。脚手架默认生成的 lib/i18n.js(见
前端多语言)内部就是订阅这个事件来自动刷新译文的。
bridge.fetch(path, options) → Promise<any>
对 /api/toolbox/tool/{tool_id}/{path} 的 fetch 简单封装——这个路径对应
manifest.json 里 api_routes 声明的自定义接口(见下方 manifest 章节)。
iframe 与宿主同源,Cookie 天然带上,不需要额外处理鉴权;响应按 Content-Type
自动解析成 JSON 或文本。
| 参数 | 类型 | 说明 |
|---|---|---|
path | str | 相对路径(不需要带前导 /),例如 "run" |
options | RequestInit | 透传给 fetch 的选项,可选 |
const rsp = await MyBooksToolBridge.fetch('run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ book_id: 42 }),
});
bridge.notify(message, level) → void
请求宿主用它自己的提示组件(Vuetify v-snackbar)展示一条消息,通过
postMessage 实现。可选能力——工具也完全可以在 iframe 内部自己画提示条。
| 参数 | 类型 | 说明 |
|---|---|---|
message | str | 提示文案 |
level | str | "success" / "error" / "info"(默认) / "warning" |
前端如何应对宿主主题 / 语言变化 #theme-locale
工具页面运行在一个和宿主完全隔离的 <iframe> 里(CSS/JS 互不影响),机制是这样的:
-
宿主渲染
<iframe>时,把当前主题和语言拼进src的 query string(作为初始值):
这样工具页面首次渲染(甚至在任何 JS 跑起来之前,如果你用<iframe src="/get/tool/{tool_id}/index.html?theme=dark&locale=zh"></iframe><html data-theme="...">这类 SSR 友好的写法)就能同步拿到当前主题/语言,不用等一次异步的postMessage握手。 -
宿主切换深浅色主题或语言时,通过
postMessage做运行时热更新,不再重新设置iframe.src——不会触发工具页面整页刷新:- 宿主页面监听
$i18n.locale/$vuetify.theme.dark变化,向 iframe 发{ source: "mybooks-toolbox-host", type: "locale-change" | "theme-change", locale | theme }; toolbox-bridge.js内部监听这个消息,更新bridge.theme/bridge.locale(用 getter 暴露,读到的始终是最新值),并触发工具通过bridge.onThemeChange/bridge.onLocaleChange注册的回调;- 工具页面内部的临时状态(表单输入等)在切换时会保留,不会像整页刷新那样丢失——
这也是从"重设
iframe.src"方案改过来的主要动机; - 旧版工具(没有调用
onThemeChange/onLocaleChange)不受影响: 读取bridge.theme/bridge.locale的方式没变,只是不会主动收到 "变了"的推送,行为退化为"首次加载后主题/语言就不再更新",不会报错、不会崩溃。
- 宿主页面监听
-
读取初始值/订阅变化都用
MyBooksToolBridge,不要自己解析location.search或监听window.onmessage:document.documentElement.dataset.theme = MyBooksToolBridge.theme; // "light" | "dark" document.documentElement.lang = MyBooksToolBridge.locale; // "zh" | "zh-TW" | "en" | ... MyBooksToolBridge.onThemeChange(function (theme) { document.documentElement.dataset.theme = theme; // 实时跟随宿主切换 }); MyBooksToolBridge.onLocaleChange(function (locale) { document.documentElement.lang = locale; }); -
样式层建议用 CSS 变量 +
[data-theme]/data-theme属性选择器,而不是 JS 逐个元素改style——脚手架模板、以及下方 共享视觉基础 theme.css 都是这个写法::root { --bg: #fff; --text: #1a1a1a; } body[data-theme="dark"] { --bg: #1a1a1a; --text: #eee; } body { background: var(--bg); color: var(--text); }
postMessage):
点一下给 <html> 设置/清除 data-theme 属性并触发一次 CSS 变量
切换——工具页面里推荐的做法一致,只是触发时机换成了 bridge.onThemeChange 回调。
工具前端的多语言(i18n)方案 #i18n
边界原则:翻译字串归工具自己所有,toolbox-bridge.js 只负责
"告诉工具当前该用哪个语言、语言变了要通知它"(bridge.locale +
bridge.onLocaleChange,见 上一节),不提供、
不托管任何字串目录——工具前端可以自由选择技术栈,一个共享的翻译渲染库会隐含对 DOM
结构/框架的假设,Vue3/React/纯 HTML 工具"如何应用一段翻译文本"的做法并不相同。
后端侧 self.api.* 抛出的、经过 _() 包过的字符串会跟着当前用户的
站点语言走(前提是这些字符串已经在 MyBooks 自己的翻译目录里);工具自己代码/前端里的字符串
不会被自动翻译,需要工具自己维护。
脚手架默认实现:frontend/lib/i18n.js + frontend/locales/*.json
mytool init 默认生成一套开箱即用、不依赖任何框架的最小 i18n
胶水代码(可以整个删掉换成自己的方案,不是强制约定):
frontend/locales/manifest.json
声明这个工具支持哪些语言与默认语言:
{ "default": "en", "locales": ["en", "zh"] }
mytool init --locales en,zh,ja(默认 en,zh)据此生成/追加对应的
frontend/locales/<code>.json;脚手架内置了 en/zh
两份示例文案,请求的其它语言代码没有内置文案时,会以英文文案为初始内容生成一份同结构的
stub 文件,交给作者自行翻译。
frontend/locales/<code>.json
扁平 key-value 字符串表,支持 {param} 占位符:
{ "action.run": "Run", "result.error": "Run failed: {error}" }
window.MyBooksToolI18n.create(options)
启动时先 fetch('locales/manifest.json'),据此解析 bridge.locale
应该匹配到哪个已声明的语言(精确匹配 → 语言前缀匹配,如 zh-CN 匹配
zh → 都没有则退回 default),再懒加载对应的
locales/<code>.json。返回的实例提供:
| 方法 | 说明 |
|---|---|
i18n.ready | Promise,首次加载完成后 resolve |
i18n.t(key, params) | 取当前语言的字串,缺失回退到 default 语言,再缺失原样返回 key;支持 {param} 插值 |
i18n.applyDom(root) | 把 root(缺省 document)内所有带 data-i18n="key" / data-i18n-attr="attr:key;attr2:key2" 的元素替换成对应译文,纯 HTML/无框架的工具可以直接用它做整页翻译 |
i18n.onChange(fn) | 语言变化时(内部已订阅 bridge.onLocaleChange 并自动重新 applyDom())额外触发一次回调,供用了前端框架、需要重新渲染而不是直接改 DOM 的工具使用 |
const i18n = window.MyBooksToolI18n.create();
i18n.ready.then(() => {
document.getElementById('status').textContent = i18n.t('status.ready', { theme: bridge.theme });
});
i18n.onChange(() => { /* 重新渲染用 i18n.t() 手写的那部分文本 */ });
manifest.json 新增可选字段
{ "locales": ["en", "zh"], "default_locale": "en" }
纯展示性质的元数据(供 mybooks.top 商店列表展示"支持语言"、/admin/toolbox
工具详情展示用),不参与后端加载/兼容性检查,不绑定 core_api_version;
mytool validate/build 只做格式性检查(是数组、
default_locale 在 locales 里),省略时不影响工具正常安装运行。
共享视觉基础:theme.css #theme-css
iframe 自包含带来 CSS/JS 完全隔离的同时,也让不同工具、以及工具与内置工具之间的视觉观感
天然缺乏一致性——新工具默认拿不到宿主的 Vuetify 组件库和主题变量。在不引入 Vuetify(会
带回版本锁定、体积膨胀等问题)的前提下,mytool init 额外提供一份轻量的
纯 CSS 视觉基础,让不同技术栈的工具"看起来是同一家产品",同时不强制任何组件库。
frontend/lib/theme.css 是一组颜色变量(--mb-color-bg/
--mb-color-primary/--mb-color-error 等,共 11 个语义色 token)+
一批可选的基础组件 class,可以整份使用、只挑变量部分接到自己已有的方案上,或整个删掉不用:
| 类别 | class |
|---|---|
| 布局 | .mb-container / .mb-row / .mb-col / .mb-spacer(简易 flex,不含 12 栏栅格) |
| 反馈类 | .mb-alert(+ --success/--error/--warning/--info)、.mb-chip、.mb-progress-linear(含 --indeterminate)、.mb-spinner |
| 结构类 | .mb-card、.mb-divider、.mb-list-item(含 __avatar/__content/__title/__subtitle/__action)、.mb-dialog-overlay/.mb-dialog |
| 表单类 | .mb-input/.mb-select/.mb-textarea、.mb-field(勾选框/单选框,靠原生 accent-color 跟主题色对齐)、.mb-file-input |
| 图标 | .mb-icon——不内置图标字体,只给"自带 SVG/图片图标"的工具一个跟文字对齐、按字号缩放的容器 |
bridge.theme/bridge.onThemeChange 给 <body>
设置 data-theme="light"|"dark",theme.css 用 CSS 自定义属性在
:root 声明浅色默认值、在 body[data-theme="dark"] 覆盖为深色值,
子元素通过变量继承自动换肤,不需要工具自己写两套样式规则或手动切换 class。
mytool init 生成的默认文件之一,不是运行时能力,也不由
toolbox-bridge.js 提供——纯粹是脚手架层面的约定,工具即使完全不使用它,
安装/加载/运行也不受任何影响。图标字体(如 MDI)不在这份基础样式的范围内。
manifest.json 字段 #manifest
工具包根目录的元数据文件,mytool init 会生成一份带占位符的模板,mytool validate/build 会校验这些字段。
| 字段 | 必填 | 说明 |
|---|---|---|
tool_id | 是 | 唯一标识,只能是小写字母/数字/下划线 |
name / description | 是 | 展示名称与描述 |
revision | 是 | 语义化版本号 x.y.z |
author | 是 | 作者 |
repo_url | 是 | 源码仓库地址,供商店审核时核对产物与公开源码是否一致 |
core_api_version | 是 | 依赖的最低 Core API 版本,见本页顶部版本号 |
entry_backend | 是 | <module>.<ClassName>,指向 backend/ 下继承 BaseTool 的类 |
entry_frontend | 否 | 前端入口文件(相对 frontend/),缺省则工具没有 UI |
page | 否 | 落地页路径,缺省用 tool_id |
api_routes | 否 | 自定义后端接口:[{"path": "...", "handler": "<module>.<ClassName>"}],对应 bridge.fetch() 请求的 /api/toolbox/tool/{tool_id}/{path} |
locales | 否 | 工具前端提供翻译文案的语言代码列表,例如 ["en", "zh"];配合 frontend/locales/<code>.json 翻译文件使用,见下方"前端多语言" |
default_locale | 否 | 没有对应 bridge.locale 翻译文件时的兜底语言,必须包含在 locales 列表里 |
mytool CLI #cli
完整命令说明见仓库 README,这里只列速查表。
| 命令 | 作用 |
|---|---|
mytool init <tool_id> | 生成一个新的工具项目骨架 |
mytool validate <path> | 只做校验,不打包;path 可以是项目目录,也可以是已有的 zip |
mytool build [dir] | 校验并打包成 dist/<tool_id>-<revision>.zip,打印 sha256 |
mytool bump <major|minor|patch> [dir] | 按 semver 规则升级 manifest.json 里的 revision |
npm install -g mybooks-tools-builder
mytool init my_tool --name "我的工具" --author "你的名字" --repo-url "https://github.com/you/my_tool"
cd my_tool
mytool validate .
mytool build