HostGateway 能力调用详解
HostGateway 是插件访问 Ting Reader 核心数据和受控能力的统一入口。这里按开发者实际调用顺序说明请求写法、权限要求、系统响应格式和运行时桥接差异。
调用入口和返回外壳
Capability 注册插件能力,决定系统什么时候调用插件;HostGateway 是插件访问宿主系统的通道,决定插件怎么安全读取或操作系统数据。
不同运行时调用的是同一组 HostGateway 方法名和参数,只是桥接方式不同。JavaScript 后台方法直接拿到业务 JSON;HTTP 中转接口会在外层包一层 result;Web 容器桥接会返回 ting-plugin:response。
| 场景 | 调用方式 | 成功时拿到什么 |
|---|---|---|
| JavaScript 后台方法 | await Ting.host.invoke(method, params) | HostGateway 方法返回的业务 JSON |
| web_container UI | postMessage({ method: "host.invoke", params: { method, params } }) | ting-plugin:response 里的 result |
| HTTP 中转接口 | POST /api/v1/plugin-host/invoke | { "result": ... } |
| WASM | ting_env.host_invoke + host_response_size + host_read_body | 读取到的 JSON 字节 |
| Native | host_invoke(method, params_json, result_json) | result_json 指向的 JSON 字符串 |
POST /api/v1/plugin-host/invoke
Content-Type: application/json
{
"plugin_id": "assistant-tools@1.0.0",
"method": "progress.recent",
"params": {
"limit": 5
}
}HTTP 中转成功响应会包一层 result:
{
"result": {
"items": [],
"total": 0,
"offset": 0,
"limit": 20
}
}Web 容器桥接成功响应如下,插件 UI 应先检查 ok:
{
"type": "ting-plugin:response",
"id": "request-id",
"ok": true,
"result": {
"items": [],
"total": 0,
"offset": 0,
"limit": 20
}
}HTTP 错误响应使用后端统一错误结构,包含错误类型、人类可读消息和 trace_id:
{
"error": "PermissionDenied",
"message": "Permission denied: Plugin assistant-tools@1.0.0 lacks permission required for host method books.list",
"trace_id": "f0b75a72-9f87-4f0b-b1bb-3df4c4fbb2b2"
}WASM 和 Native 桥接层不会抛 HTTP 响应;宿主拒绝调用时,读取到的 JSON 通常包含 error 字段:
{
"error": "Permission denied: Plugin assistant-tools@1.0.0 lacks permission required for host method books.list"
}方法与权限速查
每个 HostGateway 方法都会先匹配权限,再检查当前用户上下文。没有用户上下文的初始化、公共路由或后台调用,读取书籍、进度和库文件时会被拒绝。
| 方法 | 权限 | 说明 |
|---|---|---|
| books.list / books.get | books_read 或 database_read | 查询当前用户可访问的书籍 |
| libraries.list / libraries.get | books_read 或 database_read | 查询当前用户可访问的存储库 |
| chapters.list / chapters.get | chapters_read 或 database_read | 查询章节元数据 |
| progress.recent | progress_read 或 database_read | 读取当前用户最近播放进度 |
| media.get_url | media_read_url 或 media_read | 获取受控播放地址 |
| metadata.write | metadata_write | 创建元数据写入任务,需要管理员 |
| library.file.list / stat / read | file_read | 在本地存储库根目录内读文件 |
| library.file.write | file_write | 在本地存储库根目录内写文件,需要管理员 |
| database.get / database.list | database_read | 受控实体读取,不是裸 SQL |
| database.update | database_write | 受控实体更新,需要管理员 |
| tasks.create | task_create | 创建插件自定义后台任务 |
| cache.get / cache.has | cache_read 或 cache_write | 读取插件隔离缓存 |
| cache.set / cache.delete | cache_write | 写入或删除插件隔离缓存 |
| playlists.list / get | playlists_read 或 playlists_write | 查询当前用户的播放列表 |
| playlists.create / update / delete | playlists_write | 创建、更新或删除当前用户的播放列表 |
| playlists.add_item / remove_item | playlists_write | 向当前用户的播放列表追加或移除条目 |
| favorites.list | favorites_read 或 favorites_write | 列出当前用户的收藏 |
| favorites.add / remove | favorites_write | 加入或移除当前用户的收藏 |
| user_settings.get | user_settings_read 或 user_settings_write | 读取当前用户的设置 |
| user_settings.set | user_settings_write | 写入当前用户的单个设置项 |
书籍、存储库和章节
books.list 支持 search、tag、library_id、limit 和 offset。limit 默认 50,范围 1-200。返回分页结构:items、total、offset、limit。
const books = await Ting.host.invoke("books.list", {
search: "三体",
limit: 10,
offset: 0
});{
"items": [
{
"id": "book-id",
"title": "三体",
"author": "刘慈欣",
"narrator": "演播者",
"library_id": "library-id",
"cover_url": "/api/...",
"description": "..."
}
],
"total": 1,
"offset": 0,
"limit": 10
}books.get 使用 book_id 或 id。libraries.list 支持 limit 和 offset,管理员可以看到全部存储库,普通用户只能看到自己有权限访问的存储库。
chapters.list 必须传 book_id,limit 默认 200,最大 500;chapters.get 使用 chapter_id 或 id,并会先检查当前用户是否能访问章节所属书籍。
{
"items": [
{
"id": "chapter-id",
"book_id": "book-id",
"title": "第 1 章",
"path": "001.mp3",
"duration": 1800,
"chapter_index": 1
}
],
"total": 1,
"offset": 0,
"limit": 200
}进度和媒体地址
progress.recent 读取当前用户最近播放记录,limit 默认 20,范围 1-100。返回项包含书籍、章节、封面、位置和时长等字段。
{
"items": [
{
"id": "progress-id",
"book_id": "book-id",
"chapter_id": "chapter-id",
"position": 362,
"duration": 1800,
"updated_at": "2026-07-01T12:00:00Z",
"book_title": "三体",
"cover_url": "/api/...",
"library_id": "library-id",
"chapter_title": "第 1 章",
"chapter_duration": 1800
}
],
"limit": 20
}media.get_url 返回受控播放地址。参数使用 chapter_id 或 id;transcode 只支持 hls、mp3、wav;seek 会透传给流接口;download: true 会生成下载 URL。
{
"chapter_id": "chapter-id",
"book_id": "book-id",
"url": "/api/stream/chapter-id?transcode=hls&seek=120",
"requires_auth": true,
"auth": "current_user"
}库文件读写
库文件方法只面向本地存储库,路径必须是相对路径,不能使用绝对路径或 .. 跳出存储库根目录。library.file.read 最大读取 20 MB,library.file.write 最大写入 50 MB。
library.file.list 和 library.file.stat 返回文件条目,字段包括 name、path、is_file、is_dir、size 和 modified_unix。
const file = await Ting.host.invoke("library.file.read", {
library_id: "library-id",
path: "三体/info.json",
as_text: true
});{
"library_id": "library-id",
"path": "三体/info.json",
"size": 128,
"data_base64": "eyJ0aXRsZSI6IuS4ieS9kyJ9",
"text": "{\"title\":\"三体\"}",
"entry": {
"name": "info.json",
"path": "三体/info.json",
"is_file": true,
"is_dir": false,
"size": 128,
"modified_unix": 1782888000
}
}写入需要管理员上下文。参数可以传 text 或 data_base64,默认不覆盖已有文件;需要覆盖时传 overwrite: true。
const written = await Ting.host.invoke("library.file.write", {
library_id: "library-id",
path: "三体/plugin-note.json",
text: "{\"source\":\"plugin\"}",
overwrite: true
});受控数据库和元数据写入
HostGateway 不提供裸 SQL。database.get、database.list 和 database.update 只支持受控实体:book/books、chapter/chapters、library/libraries,database.list 额外支持 progress。
database.update 需要管理员上下文,并且只能更新白名单字段。适合插件工具对书籍标题、作者、封面、标签、章节标题等已建模字段做修正。
const updated = await Ting.host.invoke("database.update", {
entity: "book",
id: "book-id",
patch: {
title: "三体",
author: "刘慈欣",
tags: "科幻,中文"
}
});metadata.write 不直接写文件,而是创建核心元数据写入任务。这样可以复用系统队列、日志和失败重试。
{
"task_id": "task-id",
"task_type": "write_metadata",
"status": "queued",
"book_id": "book-id"
}任务与缓存
tasks.create 只能创建插件自定义任务,library_scan 和 write_metadata 是核心保留任务类型。目标任务类型必须已经有插件声明 task_handler.task_types。
const task = await Ting.host.invoke("tasks.create", {
task_type: "plugin.summarize",
name: "生成书籍摘要",
priority: "normal",
data: {
book_id: "book-id"
}
});{
"task_id": "task-id",
"task_type": "plugin.summarize",
"status": "queued",
"handler_count": 1
}缓存按插件实例隔离,assistant-tools@1.0.0 和 assistant-tools@1.0.1 是不同命名空间。cache.get 命中时返回 value 和时间戳,未命中返回 hit: false 与 value: null。
await Ting.host.invoke("cache.set", {
key: "last-search",
value: {
query: "三体",
total: 3
}
});
const cached = await Ting.host.invoke("cache.get", {
key: "last-search"
});{
"hit": true,
"key": "last-search",
"value": {
"query": "三体",
"total": 3
},
"created_at": "2026-07-01T12:00:00Z",
"updated_at": "2026-07-01T12:00:00Z"
}个人数据:播放列表、收藏和用户设置
播放列表、收藏和用户设置方法都绑定当前登录用户,插件只能操作调用者自己的数据,不需要管理员权限。写入前会校验 playlist.user_id == user.id,越权返回 PermissionDenied。
playlists.create 支持一次性传入 items 数组(item_type 为 book 或 series),创建后自动插入。playlists.add_item 和 playlists.remove_item 用于增量修改。
const playlist = await Ting.host.invoke("playlists.create", {
name: "姬叉后宫文推荐",
description: "由 AI 书单助手生成",
items: [
{ item_type: "book", item_id: "68ed6f15-f939-450f-9aab-f3ce49c2b25e" },
{ item_type: "book", item_id: "3d4ff97d-029f-485f-85c4-7bfcb755451f" }
]
});{
"id": "pl-a1b2c3d4",
"name": "姬叉后宫文推荐",
"description": "由 AI 书单助手生成",
"user_id": "u-1234",
"created_at": "2026-07-02T12:00:00Z",
"updated_at": "2026-07-02T12:00:00Z",
"items": [
{ "item_type": "book", "item_id": "68ed6f15-f939-450f-9aab-f3ce49c2b25e", "item_order": 0 },
{ "item_type": "book", "item_id": "3d4ff97d-029f-485f-85c4-7bfcb755451f", "item_order": 1 }
]
}favorites.add 会先校验当前用户是否能访问该书籍,重复添加时返回 { ok: true, created: false }。favorites.list 返回 { items, total }。
await Ting.host.invoke("favorites.add", { book_id: "68ed6f15-f939-450f-9aab-f3ce49c2b25e" });
const favs = await Ting.host.invoke("favorites.list", { limit: 50 });user_settings.set 的 value 可为字符串、数字、布尔或对象,宿主会 JSON 编码后写入。保留字段 user_id、updated_at、settings_json 不能作为 key。user_settings.get 不带 key 时返回全部设置的 { key: value } 映射。
await Ting.host.invoke("user_settings.set", {
key: "ai_booklist_prefers_narrator",
value: "头陀渊讲故事"
});
const all = await Ting.host.invoke("user_settings.get", {});Web 容器桥接
web_container UI 页面不能直接拿到后端对象,需要通过 postMessage 调用宿主。method: "host.invoke" 调 HostGateway,method: "capability.invoke" 调当前或指定 capability。
页面加载后宿主会发送 ting-plugin:init。Web 端当前包含插件、capability、slot 和上下文;Flutter 端还会附带可选 theme,并在主题变化时发送 ting-plugin:theme。
{
"type": "ting-plugin:init",
"pluginId": "assistant-tools@1.0.0",
"pluginName": "Assistant Tools",
"capabilityId": "assistant.panel",
"slot": "global.floating_action",
"contexts": ["global"],
"context": {
"book_id": "book-id"
},
"theme": {
"colorScheme": "dark",
"brightness": "dark",
"cssVariables": {
"--bg": "#020617",
"--panel": "#0f172a",
"--text": "#f8fafc"
}
}
}const id = crypto.randomUUID();
window.parent.postMessage({
type: "ting-plugin:request",
id,
method: "host.invoke",
params: {
method: "progress.recent",
params: { limit: 5 }
}
}, "*");
window.addEventListener("message", (event) => {
const message = event.data;
if (message?.type === "ting-plugin:response" && message.id === id) {
if (!message.ok) throw new Error(message.error);
console.log(message.result);
}
});外部链接使用普通 <a target="_blank" rel="noopener noreferrer"> 即可。Web 端 iframe 允许弹窗;Flutter 端会把非插件资产的 http/https 导航、target="_blank" 和 window.open() 转给系统浏览器。
<a href="https://example.com/register" target="_blank" rel="noopener noreferrer">
注册服务
</a>WASM 和 Native 桥接
WASM 的 host_invoke 返回响应句柄;大于 0 表示可以继续读取 JSON,负数表示桥接层错误。读取 JSON 后,如果里面有 error 字段,也应按业务失败处理。
#[link(wasm_import_module = "ting_env")]
extern "C" {
fn host_invoke(
method_ptr: *const u8,
method_len: i32,
params_ptr: *const u8,
params_len: i32,
) -> i32;
fn host_response_size(handle: i32) -> i32;
fn host_read_body(handle: i32, ptr: *mut u8, len: i32) -> i32;
}| WASM 错误码 | 含义 |
|---|---|
| -1 | WASM 内存访问失败 |
| -2 | 字符串不是合法 UTF-8 |
| -3 | 参数不是合法 JSON |
| -8 | HostGateway 未配置 |
| -9 | 当前调用缺少认证用户上下文 |
| -10 | 当前线程没有 Tokio runtime |
| -11 | 宿主调用线程异常 |
| -12 | 宿主响应序列化失败 |
Native 运行时通过 plugin_set_host_api 接收宿主 API。host_invoke 返回 0 表示成功,负数表示桥接层或 HostGateway 调用失败;只要 result_json 非空,就应读取 JSON 并在用完后调用 host_free。
#[repr(C)]
pub struct TingNativeHostApi {
pub version: u32,
pub host_invoke: Option<unsafe extern "C" fn(
method: *const c_char,
params_json: *const c_char,
result_json: *mut *mut c_char,
) -> i32>,
pub host_free: Option<unsafe extern "C" fn(ptr: *mut c_char)>,
}| Native 错误码 | 含义 |
|---|---|
| -1 | 入参指针为空 |
| -2 | Native HostGateway context 未激活 |
| -3 | 插件没有配置 HostGateway |
| -4 | 当前调用缺少认证用户上下文 |
| -5 | 字符串或 JSON 参数解析失败 |
| -6 | HostGateway 调用失败,详情在 result_json.error |
| -7 | 响应 JSON 序列化或 CString 构造失败 |