MyBooks Toolbox Core API
给 MyBooks Toolbox 工具(内置工具与外部工具)用的后端 API(self.api.*)和
前端桥接脚本(toolbox-bridge.js)参考文档。脚手架 CLI 见本仓库
README。
1.4.0
CLI 命令 mytool
类型 builtin / tool
同步自 mybooks/mybooks · 见页尾
manifest.json 里的
core_api_version 声明的是"工具依赖的最低 Core API 版本",每个 MyBooks
发行版捆绑固定的 CORE_API_VERSION,新方法只会出现在更高的发行版里。
| Core API 版本 | 起始支持的 MyBooks 发行版 |
|---|---|
1.3.0 | v4.3.1 |
1.4.0 | v4.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。
| 参数 | 类型 | 说明 |
|---|---|---|
title | str | 书名 |
authors | list[str] | None | 作者列表,缺省时为一个本地化的"未知作者" |
calibre.search_books(query, max_results=20, include_comments=False) → list[dict]
按 Calibre 搜索语法查询书籍,返回 dict 列表(含 id/title/authors/formats 等字段)。
| 参数 | 类型 | 说明 |
|---|---|---|
query | str | Calibre 搜索表达式,如 tag:小说 |
max_results | int | 最多返回的书籍数,默认 20 |
include_comments | bool | 是否在结果里带上简介字段,默认 False |
calibre.search_ids(query) → list[int]
按 Calibre 搜索语法查询书籍,只返回排序后的 book_id 列表,不取详情,适合只需要遍历 id 的场景(比全量取详情快)。
| 参数 | 类型 | 说明 |
|---|---|---|
query | str | Calibre 搜索表达式 |
calibre.get_metadata(book_id, get_cover=False, cover_as_data=False) → BookMetadata
返回指定书籍的元数据对象(类型见上方 BookMetadata 说明)。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
get_cover | bool | 是否附带封面(返回对象上会带封面路径/数据,取决于 cover_as_data),默认 False |
cover_as_data | bool | 为 True 时封面以内存字节形式返回,而不是磁盘路径;仅在 get_cover=True 时有意义,默认 False |
calibre.set_metadata(book_id, mi, force_changes=True) → None
写回书籍元数据。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
mi | BookMetadata | 新的元数据对象,通常是 get_metadata/new_metadata 得到的实例改好字段后传回 |
force_changes | bool | 是否强制写入未显式标记为"已修改"的字段,默认 True,保证调用方直接改属性即可生效 |
calibre.get_data_as_dict(ids) → list[dict]
按 book_id 列表批量返回书籍 dict(含 available_formats/title 等字段),比逐个 get_metadata 更省调用次数;返回顺序与 ids 无关。
| 参数 | 类型 | 说明 |
|---|---|---|
ids | list[int] | 书籍 id 列表 |
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 直接取值。只需单个字段时优先用本方法。
| 参数 | 类型 | 说明 |
|---|---|---|
field | str | Calibre 字段名。内置字段直接写名字(如 "tags"/"series"/"languages");自定义列须带 # 前缀(如 "#category"),与 get_custom 的 label 不同(不带 #) |
book_ids | list[int] | 书籍 id 列表;为空时返回空 dict,不会回退为全部书籍——全库统计需自行传入全部 id(如 all_book_ids()) |
返回 {book_id: value};值类型取决于字段(多值字段为 tuple,无值为
None 或空 tuple)。字段不存在时抛出 KeyError。
calibre.cover(book_id) → bytes | None
返回书籍封面的原始字节;没有封面时返回 None。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
calibre.set_cover(book_id, cover) → None
设置书籍封面。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
cover | bytes | 封面图片的原始字节(如 JPEG/PNG 数据) |
calibre.import_book(mi, formats) → int | None
将本地格式文件与给定元数据一并入库,返回新书的 book_id;入库失败时返回 None。
| 参数 | 类型 | 说明 |
|---|---|---|
mi | BookMetadata | 新书的元数据,通常由 new_metadata 构造 |
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 等工具在用)。
| 参数 | 类型 | 说明 |
|---|---|---|
user_id | int | 发起导入的用户 id(用于记录/权限相关上下文) |
file_path | str | 待导入文件的绝对路径 |
title | str | 书名 |
authors | list[str] | 作者列表 |
delete_after_import | bool | 导入成功后是否删除原始文件,默认 True |
返回新书的 book_id。
calibre.merge_formats(source_book_id, target_book_id) → list[str]
把源书的格式文件合并进目标书;合并后源书里对应格式被移动,不再重复保留。
| 参数 | 类型 | 说明 |
|---|---|---|
source_book_id | int | 源书 book_id |
target_book_id | int | 目标书 book_id |
返回实际合并过去的格式名列表。
calibre.add_format(book_id, fmt, file_path, replace=True) → None
给书籍添加一个格式文件。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
fmt | str | 格式名(如 "EPUB"),大小写不敏感,Calibre 内部会统一处理 |
file_path | str | 本地格式文件的绝对路径 |
replace | bool | 该书已存在同名格式时是否替换,默认 True;传 False 时若已存在同格式则不做任何修改 |
calibre.format_abspath(book_id, fmt) → str | None
返回某本书某个格式文件在磁盘上的绝对路径;该格式不存在时返回 None。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
fmt | str | 格式名(如 "EPUB") |
calibre.get_custom(book_id, label) → any
读取单本书某个自定义列(Calibre 里以 # 开头的列,这里传不带 # 的 label)的值;返回类型取决于列本身(文本/数字/布尔/列表等)。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
label | str | 自定义列名,不带 # 前缀 |
系统内已有的自定义列参见 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
批量写入自定义列的值。
| 参数 | 类型 | 说明 |
|---|---|---|
label | str | 自定义列名,不带 # 前缀(同上方 get_custom 列出的 CALIBRE_COLUMN_* 常量) |
values | dict | {book_id: value},一次性写入多本书 |
calibre.remove_formats(values) → None
批量删除格式文件。
| 参数 | 类型 | 说明 |
|---|---|---|
values | dict | {book_id: [fmt, ...]},fmt 为格式名(如 "EPUB") |
calibre.set_language(book_id, language) → None
设置书籍语言。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 id |
language | str | 语言代码(如 "zho"/"eng",ISO 639-2/B) |
calibre.all_book_ids() → list[int]
返回书库中所有书籍的 ID 列表。批量扫描类工具(格式精简、语言检测等)常用这个做遍历入口。
calibre.delete_book(book_id) → None
从 Calibre 书库及应用自己的 Item 表中一并删除指定书籍,不可撤销,请在有明确用户确认的操作里使用。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | 书籍 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_id | int | Calibre 书籍 id |
db.create_item(book_id, collector_id) → dict
为一本书创建 Item 记录(登记入库人)并持久化,返回新建记录的 {"id", "book_id", "collector_id"}。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | Calibre 书籍 id |
collector_id | int | 入库人(Reader)的用户 id |
db.delete_item_by_book_id(book_id) → None
删除某本书对应的 Item 记录(不存在时静默跳过)。
| 参数 | 类型 | 说明 |
|---|---|---|
book_id | int | Calibre 书籍 id |
db.get_reader(user_id) → dict | None
按用户 id 查找 Reader(应用用户账号);返回 {"id", "username", "name", "admin"},不存在时返回 None。
| 参数 | 类型 | 说明 |
|---|---|---|
user_id | int | 用户 id |
self.api.tasks 后台任务
耗时操作(下载、格式转换、批量扫描等)应该跑成后台任务,让用户能在任务面板看到进度,而不是让 HTTP 请求一直挂起。
tasks.create_task(progress_data=None) → int
创建一个后台任务记录供前端轮询进度,返回 task_id。任务名称取自工具类的 service_item_name 属性;后台任务面板里该工具的 service_type 自动带 "tool:<tool_id>" 前缀,与内置工具区分展示。
| 参数 | 类型 | 说明 |
|---|---|---|
progress_data | dict | None | 任务的初始自定义数据(如描述、总数),可为 None |
tasks.update_progress(task_id, progress, progress_data=None) → None
更新任务进度。
| 参数 | 类型 | 说明 |
|---|---|---|
task_id | int | create_task 返回的任务 id |
progress | int | 进度百分比(0–100) |
progress_data | dict | None | 本次一并更新的自定义数据,可为 None(不更新) |
tasks.complete_task(task_id, error_message=None) → None
标记任务结束;传 error_message 则标记为失败并附带错误描述。
| 参数 | 类型 | 说明 |
|---|---|---|
task_id | int | create_task 返回的任务 id |
error_message | str | None | 失败原因;为 None 时视为成功完成 |
tasks.make_progress_callback(task_id, progress_data_factory=None, outer_callback=None) → callable
构造一个 (progress: int) -> None 的回调函数,便于把进度上报接进那些只接受单参数回调的循环/子调用里,消除手写进度回调时重复的样板代码;常见于给下载器/转换器传 callback= 参数。
| 参数 | 类型 | 说明 |
|---|---|---|
task_id | int | create_task 返回的任务 id |
progress_data_factory | callable | None | 可选,按当前 progress 生成本次的 progress_data |
outer_callback | callable | None | 可选,在更新进度之后再额外调用一次的回调(如日志/中止检查) |
self.api.messages 站内消息
给用户发一条持久化的站内通知,独立于任务面板(例如任务已经 complete,但还想额外提示一句"已生成新书,可在书库中查看")。
messages.send_message(user_id, msg, status="info") → None
给用户发一条站内信;发送前会先清理该用户同内容的旧消息(避免重复刷屏)。user_id
为假值(0/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 内部已经会做一次同内容去重,一般不需要单独调用。
| 参数 | 类型 | 说明 |
|---|---|---|
user_id | int | 用户 id |
msg_content | str | 要匹配清理的消息正文 |
days | int | 保留天数,早于该天数的同内容消息会被删除,默认 31 |
self.api.storage 工具专属存储
每个工具都有自己独立的数据目录,路径由 tool_id 派生,不同工具之间互不干扰。
storage.get_work_dir(unique_key=None) → str
获取(并按需创建)该工具专属的临时工作目录。
| 参数 | 类型 | 说明 |
|---|---|---|
unique_key | str | None | 可选的区分键(如任务 id),传入时每个 key 对应独立的子目录,避免并发任务互相覆盖文件;不传则返回工具共享的工作目录 |
storage.cleanup_work_dir(work_dir) → None
删除 get_work_dir 创建的目录及其内容;失败只记录警告,不向上抛异常(避免因为清理失败影响主流程的成功状态)。
| 参数 | 类型 | 说明 |
|---|---|---|
work_dir | str | get_work_dir 返回的目录路径 |
storage.get_config() → dict
读取该工具的持久配置(config.json),文件不存在或读取失败时返回 {},不抛异常。
storage.set_config(data) → None
整份覆盖写入该工具的持久配置(config.json,UTF-8,缩进 2);适合保存 API Key、用户偏好这类"重启也要记住"的设置。
| 参数 | 类型 | 说明 |
|---|---|---|
data | dict | 要保存的配置内容,会以 JSON 整体覆盖旧文件 |
写入失败会抛 RuntimeError(原始异常作为 __cause__)。
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。
| 参数 | 类型 | 说明 |
|---|---|---|
s | str | 待处理的字符串 |
utils.get_title_sort(title) → str
把书名转成用于排序的 ASCII 小写形式(内部走 Calibre 的 unicode-decomposition),转换失败时原样返回 title,不抛异常。
| 参数 | 类型 | 说明 |
|---|---|---|
title | str | 原始书名 |
utils.guess_title_author_from_filename(name) → (str, str|None)
从"《书名》作者:某某"这类常见命名习惯的文件名里拆出 (title, author);name 里不含"作者"关键字时,author 返回 None,title 原样返回(去除首尾空白)。
| 参数 | 类型 | 说明 |
|---|---|---|
name | str | 文件名(不含目录,可含或不含扩展名) |
utils.parse_date(date_str) → datetime|None
按常见格式(含中文"年月日",如 2024年5月1日/2024-05-01/2024/05)解析日期字符串,返回带 UTC 时区的 datetime;解析不出来或传空值时返回 None。
| 参数 | 类型 | 说明 |
|---|---|---|
date_str | str | 日期字符串,如 "2024-01-01"、"2024年1月1日" |
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