🧰 MyBooks Toolbox Core API

MyBooks Toolbox Core API

MyBooks Toolbox 工具(内置工具与外部工具)用的后端 API(self.api.*)和 前端桥接脚本(toolbox-bridge.js)参考文档。脚手架 CLI 见本仓库 README

Core API 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 等字段)。

参数类型说明
querystrCalibre 搜索表达式,如 authors:="张三"
max_resultsint最多返回条数,默认 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_idintCalibre 书籍 ID
get_coverbool是否附带封面(写入磁盘临时文件路径),默认 False
cover_as_databool封面以内存字节形式附带(配合 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_idint书籍 ID
coverbytes封面原始字节
calibre.import_book(mi, formats) → int | None

将本地格式文件与给定元数据一并入库,返回新书的 book_id

参数类型说明
miBookMetadata要写入的元数据对象
formatslist[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_idint接收用户 ID
msgstr消息内容
statusstr"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
参数类型说明
keystr配置项名称,必须在白名单内才读得到真实值
defaultAnykey 不在白名单内,或 CONF 里没有这一项时返回的兜底值

当前白名单(源头见 mybooks/mybooks 仓库 webserver/toolbox/core_api.pySettingsAPI.ALLOWED_KEYS):

key说明
auto_fill_meta是否自动补全导入书籍的元数据
audio_output_folder音频类工具的输出目录路径
DEFAULT_LANGUAGE站点默认语言,可用作工具自行做多语言展示的兜底
需要读一个不在名单里的 key?这属于新的功能需求,请在 mybooks/mybooks 开 issue 请求加白名单,而不是自己找办法绕过 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 返回 Nonetitle 原样返回(去除首尾空白)。

utils.parse_date(date_str) → datetime|None

按常见格式(含中文"年月日",如 2024年5月1日/2024-05-01/2024/05)解析日期字符串,返回带 UTC 时区的 datetime;解析不出来或传空值时返回 None

没有开放的能力:Core API 不提供任意 SQL / ORM 查询的"逃生舱"——只暴露具名方法。 如果你的工具需要一个这里没有的能力,说明这是一个值得补充到 Core API 的通用需求,请在 mybooks/mybooks 开 issue,而不是绕过 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.jsonapi_routes 声明的自定义接口(见下方 manifest 章节)。 iframe 与宿主同源,Cookie 天然带上,不需要额外处理鉴权;响应按 Content-Type 自动解析成 JSON 或文本。

参数类型说明
pathstr相对路径(不需要带前导 /),例如 "run"
optionsRequestInit透传给 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 内部自己画提示条。

参数类型说明
messagestr提示文案
levelstr"success" / "error" / "info"(默认) / "warning"

前端如何应对宿主主题 / 语言变化 #theme-locale

工具页面运行在一个和宿主完全隔离的 <iframe> 里(CSS/JS 互不影响),机制是这样的:

  1. 宿主渲染 <iframe> 时,把当前主题和语言拼进 src 的 query string(作为初始值):
    <iframe src="/get/tool/{tool_id}/index.html?theme=dark&locale=zh"></iframe>
    这样工具页面首次渲染(甚至在任何 JS 跑起来之前,如果你用 <html data-theme="..."> 这类 SSR 友好的写法)就能同步拿到当前主题/语言,不用等一次异步的 postMessage 握手。
  2. 宿主切换深浅色主题或语言时,通过 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 的方式没变,只是不会主动收到 "变了"的推送,行为退化为"首次加载后主题/语言就不再更新",不会报错、不会崩溃。
  3. 读取初始值/订阅变化都用 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;
    });
  4. 样式层建议用 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.readyPromise,首次加载完成后 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_versionmytool validate/build 只做格式性检查(是数组、 default_localelocales 里),省略时不影响工具正常安装运行。

共享视觉基础: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 列表里
locales/default_locale 两个字段的完整用法见 前端多语言一节。

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