🧰 MyBooks Toolbox Core API

MyBooks Toolbox Core API

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

Core API 1.4.0 CLI 命令 mytool 类型 builtin / tool 同步自 mybooks/mybooks · 见页尾
Core API 版本 ↔ MyBooks 发行版:manifest.json 里的 core_api_version 声明的是"工具依赖的最低 Core API 版本",每个 MyBooks 发行版捆绑固定的 CORE_API_VERSION,新方法只会出现在更高的发行版里。
Core API 版本起始支持的 MyBooks 发行版
1.3.0v4.3.1
1.4.0v4.4.2

表的来源是 mybooks/mybooks 仓库 webserver/toolbox/core_api.py 顶部 CORE_API_VERSION 常量旁边的注释;mytool validate/ build 目前只检查 core_api_version 是不是合法的语义化版本号 (x.y.z),不会拿它和目标 MyBooks 实际运行的 CORE_API_VERSION 做比对——写工具时自己对照这张表,把 core_api_version 设成你实际用到的最新方法对应的版本号;如果填低了, 工具会在用户的 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 书库读写

查询、导入、修改书籍元数据与格式文件。

下面 new_metadata/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.new_metadata(title, authors=None) → BookMetadata

构造一个新的元数据对象,常见于"生成新书"场景,配合 import_book 使用;把 Metadata(title, authors) 的构造逻辑收在这里,调用方因此不需要自己 import calibre。

参数类型说明
titlestr书名
authorslist[str] | None作者列表,缺省时为一个本地化的"未知作者"
calibre.search_books(query, max_results=20, include_comments=False) → list[dict]

按 Calibre 搜索语法查询书籍,返回 dict 列表(含 id/title/authors/formats 等字段)。

参数类型说明
querystrCalibre 搜索表达式,如 tag:小说
max_resultsint最多返回的书籍数,默认 20
include_commentsbool是否在结果里带上简介字段,默认 False
calibre.search_ids(query) → list[int]

按 Calibre 搜索语法查询书籍,只返回排序后的 book_id 列表,不取详情,适合只需要遍历 id 的场景(比全量取详情快)。

参数类型说明
querystrCalibre 搜索表达式
calibre.get_metadata(book_id, get_cover=False, cover_as_data=False) → BookMetadata

返回指定书籍的元数据对象(类型见上方 BookMetadata 说明)。

参数类型说明
book_idint书籍 id
get_coverbool是否附带封面(返回对象上会带封面路径/数据,取决于 cover_as_data),默认 False
cover_as_databool为 True 时封面以内存字节形式返回,而不是磁盘路径;仅在 get_cover=True 时有意义,默认 False
calibre.set_metadata(book_id, mi, force_changes=True) → None

写回书籍元数据。

参数类型说明
book_idint书籍 id
miBookMetadata新的元数据对象,通常是 get_metadata/new_metadata 得到的实例改好字段后传回
force_changesbool是否强制写入未显式标记为"已修改"的字段,默认 True,保证调用方直接改属性即可生效
calibre.get_data_as_dict(ids) → list[dict]

按 book_id 列表批量返回书籍 dict(含 available_formats/title 等字段),比逐个 get_metadata 更省调用次数;返回顺序与 ids 无关。

参数类型说明
idslist[int]书籍 id 列表
Calibre 底层实现是遍历整个书库、逐条判断 id in ids 后再过滤——即使 ids 只有几本书,耗时也和书库总量成正比;在循环里反复传 [book_id] 单本调用会退化成 O(N²),应该一次性传入全部 id。只需要读取单个字段(而不是完整的 书籍 dict)时,优先用下面的 get_field_map,开销小得多。
calibre.get_field_map(field, book_ids) → dict[int, any]

批量读取同一个字段在多本书上的值,不构造完整元数据,适合全库统计。相比 get_data_as_dict——后者为每本书构造包含所有字段(格式、路径、全部自定义列等) 的完整 dict,书多时耗时、耗内存;本方法只读取一个字段,直接走内存缓存,开销小得多, 且返回 {book_id: value},可按 id 直接取值。只需单个字段时优先用本方法。

参数类型说明
fieldstrCalibre 字段名。内置字段直接写名字(如 "tags"/"series"/"languages");自定义列须带 # 前缀(如 "#category"),与 get_custom 的 label 不同(不带 #)
book_idslist[int]书籍 id 列表;为空时返回空 dict,不会回退为全部书籍——全库统计需自行传入全部 id(如 all_book_ids())

返回 {book_id: value};值类型取决于字段(多值字段为 tuple,无值为 None 或空 tuple)。字段不存在时抛出 KeyError。

calibre.cover(book_id) → bytes | None

返回书籍封面的原始字节;没有封面时返回 None。

参数类型说明
book_idint书籍 id
calibre.set_cover(book_id, cover) → None

设置书籍封面。

参数类型说明
book_idint书籍 id
coverbytes封面图片的原始字节(如 JPEG/PNG 数据)
calibre.import_book(mi, formats) → int | None

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

参数类型说明
miBookMetadata新书的元数据,通常由 new_metadata 构造
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 等工具在用)。

参数类型说明
user_idint发起导入的用户 id(用于记录/权限相关上下文)
file_pathstr待导入文件的绝对路径
titlestr书名
authorslist[str]作者列表
delete_after_importbool导入成功后是否删除原始文件,默认 True

返回新书的 book_id。

calibre.merge_formats(source_book_id, target_book_id) → list[str]

把源书的格式文件合并进目标书;合并后源书里对应格式被移动,不再重复保留。

参数类型说明
source_book_idint源书 book_id
target_book_idint目标书 book_id

返回实际合并过去的格式名列表。

calibre.add_format(book_id, fmt, file_path, replace=True) → None

给书籍添加一个格式文件。

参数类型说明
book_idint书籍 id
fmtstr格式名(如 "EPUB"),大小写不敏感,Calibre 内部会统一处理
file_pathstr本地格式文件的绝对路径
replacebool该书已存在同名格式时是否替换,默认 True;传 False 时若已存在同格式则不做任何修改
calibre.format_abspath(book_id, fmt) → str | None

返回某本书某个格式文件在磁盘上的绝对路径;该格式不存在时返回 None。

参数类型说明
book_idint书籍 id
fmtstr格式名(如 "EPUB")
calibre.get_custom(book_id, label) → any

读取单本书某个自定义列(Calibre 里以 # 开头的列,这里传不带 # 的 label)的值;返回类型取决于列本身(文本/数字/布尔/列表等)。

参数类型说明
book_idint书籍 id
labelstr自定义列名,不带 # 前缀

系统内已有的自定义列参见 webserver/constants.py 里的 CALIBRE_COLUMN_* 常量(去掉 # 前缀即为这里的 label):CALIBRE_COLUMN_CATEGORY (#category)、CALIBRE_COLUMN_FOLDER (#folder)、 CALIBRE_COLUMN_BOOK_TYPE (#book_type)、 CALIBRE_COLUMN_PHY_COUNT (#book_count)、 CALIBRE_COLUMN_EXT_LINK (#ext_link)、 CALIBRE_COLUMN_LOCATION (#location)、 CALIBRE_COLUMN_TRANSLATORS (#translators)、 CALIBRE_COLUMN_CATALOG (#catalog)。

calibre.set_custom(label, values) → None

批量写入自定义列的值。

参数类型说明
labelstr自定义列名,不带 # 前缀(同上方 get_custom 列出的 CALIBRE_COLUMN_* 常量)
valuesdict{book_id: value},一次性写入多本书
calibre.remove_formats(values) → None

批量删除格式文件。

参数类型说明
valuesdict{book_id: [fmt, ...]},fmt 为格式名(如 "EPUB")
calibre.set_language(book_id, language) → None

设置书籍语言。

参数类型说明
book_idint书籍 id
languagestr语言代码(如 "zho"/"eng",ISO 639-2/B)
calibre.all_book_ids() → list[int]

返回书库中所有书籍的 ID 列表。批量扫描类工具(格式精简、语言检测等)常用这个做遍历入口。

calibre.delete_book(book_id) → None

从 Calibre 书库及应用自己的 Item 表中一并删除指定书籍,不可撤销,请在有明确用户确认的操作里使用。

参数类型说明
book_idint书籍 id

self.api.db 应用数据库

读写应用自己的 SQLAlchemy 模型(Reader / Item),不暴露裸的 Session 或 ORM 对象——全部返回/接收 plain dict,避免工具持有脱离 session 生命周期的实例。

db.get_item_by_book_id(book_id) → dict | None

按 book_id 查找对应的 Item 记录(应用侧对每本 Calibre 书籍的附加信息,如入库人);返回 {"id", "book_id", "collector_id"},不存在时返回 None。

参数类型说明
book_idintCalibre 书籍 id
db.create_item(book_id, collector_id) → dict

为一本书创建 Item 记录(登记入库人)并持久化,返回新建记录的 {"id", "book_id", "collector_id"}。

参数类型说明
book_idintCalibre 书籍 id
collector_idint入库人(Reader)的用户 id
db.delete_item_by_book_id(book_id) → None

删除某本书对应的 Item 记录(不存在时静默跳过)。

参数类型说明
book_idintCalibre 书籍 id
db.get_reader(user_id) → dict | None

按用户 id 查找 Reader(应用用户账号);返回 {"id", "username", "name", "admin"},不存在时返回 None。

参数类型说明
user_idint用户 id

self.api.tasks 后台任务

耗时操作(下载、格式转换、批量扫描等)应该跑成后台任务,让用户能在任务面板看到进度,而不是让 HTTP 请求一直挂起。

tasks.create_task(progress_data=None) → int

创建一个后台任务记录供前端轮询进度,返回 task_id。任务名称取自工具类的 service_item_name 属性;后台任务面板里该工具的 service_type 自动带 "tool:<tool_id>" 前缀,与内置工具区分展示。

参数类型说明
progress_datadict | None任务的初始自定义数据(如描述、总数),可为 None
tasks.update_progress(task_id, progress, progress_data=None) → None

更新任务进度。

参数类型说明
task_idintcreate_task 返回的任务 id
progressint进度百分比(0–100)
progress_datadict | None本次一并更新的自定义数据,可为 None(不更新)
tasks.complete_task(task_id, error_message=None) → None

标记任务结束;传 error_message 则标记为失败并附带错误描述。

参数类型说明
task_idintcreate_task 返回的任务 id
error_messagestr | None失败原因;为 None 时视为成功完成
tasks.make_progress_callback(task_id, progress_data_factory=None, outer_callback=None) → callable

构造一个 (progress: int) -> None 的回调函数,便于把进度上报接进那些只接受单参数回调的循环/子调用里,消除手写进度回调时重复的样板代码;常见于给下载器/转换器传 callback= 参数。

参数类型说明
task_idintcreate_task 返回的任务 id
progress_data_factorycallable | None可选,按当前 progress 生成本次的 progress_data
outer_callbackcallable | None可选,在更新进度之后再额外调用一次的回调(如日志/中止检查)

self.api.messages 站内消息

给用户发一条持久化的站内通知,独立于任务面板(例如任务已经 complete,但还想额外提示一句"已生成新书,可在书库中查看")。

messages.send_message(user_id, msg, status="info") → None

给用户发一条站内信;发送前会先清理该用户同内容的旧消息(避免重复刷屏)。user_id 为假值(0/None)时直接跳过,不发送。

参数类型说明
user_idint接收用户 ID;为假值时跳过
msgstr消息内容
statusstr"info" / "success" / "warning" / "danger",决定前端展示的样式
messages.cleanup_messages(user_id, msg_content, days=31) → int

删除用户 days 天前发送过的同内容旧消息,返回实际删除的消息条数。send_message 内部已经会做一次同内容去重,一般不需要单独调用。

参数类型说明
user_idint用户 id
msg_contentstr要匹配清理的消息正文
daysint保留天数,早于该天数的同内容消息会被删除,默认 31

self.api.storage 工具专属存储

每个工具都有自己独立的数据目录,路径由 tool_id 派生,不同工具之间互不干扰。

storage.get_work_dir(unique_key=None) → str

获取(并按需创建)该工具专属的临时工作目录。

参数类型说明
unique_keystr | None可选的区分键(如任务 id),传入时每个 key 对应独立的子目录,避免并发任务互相覆盖文件;不传则返回工具共享的工作目录
storage.cleanup_work_dir(work_dir) → None

删除 get_work_dir 创建的目录及其内容;失败只记录警告,不向上抛异常(避免因为清理失败影响主流程的成功状态)。

参数类型说明
work_dirstrget_work_dir 返回的目录路径
storage.get_config() → dict

读取该工具的持久配置(config.json),文件不存在或读取失败时返回 {},不抛异常。

storage.set_config(data) → None

整份覆盖写入该工具的持久配置(config.json,UTF-8,缩进 2);适合保存 API Key、用户偏好这类"重启也要记住"的设置。

参数类型说明
datadict要保存的配置内容,会以 JSON 整体覆盖旧文件

写入失败会抛 RuntimeError(原始异常作为 __cause__)。

self.api.settings 系统配置只读白名单

MyBooks 的系统配置(数据库连接串、各种第三方 API key/token 等)不能整份透给工具, 所以这里只开放一个白名单:只有显式登记过的 key 才能读到,其余一律返回调用方传入的 default,不会抛异常。

settings.get(key, default=None) → Any
参数类型说明
keystr配置项名称,必须在白名单内才读得到真实值
defaultAnykey 不在白名单内,或 CONF 里没有这一项时返回的兜底值

当前白名单(源头见 mybooks/mybooks 仓库 webserver/toolbox/core_api.py 的 SettingsAPI.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。

参数类型说明
sstr待处理的字符串
utils.get_title_sort(title) → str

把书名转成用于排序的 ASCII 小写形式(内部走 Calibre 的 unicode-decomposition),转换失败时原样返回 title,不抛异常。

参数类型说明
titlestr原始书名
utils.guess_title_author_from_filename(name) → (str, str|None)

从"《书名》作者:某某"这类常见命名习惯的文件名里拆出 (title, author);name 里不含"作者"关键字时,author 返回 None,title 原样返回(去除首尾空白)。

参数类型说明
namestr文件名(不含目录,可含或不含扩展名)
utils.parse_date(date_str) → datetime|None

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

参数类型说明
date_strstr日期字符串,如 "2024-01-01"、"2024年1月1日"
没有开放的能力: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.json 里 api_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_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 列表里
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