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 UIpostMessage({ method: "host.invoke", params: { method, params } })ting-plugin:response 里的 result
HTTP 中转接口POST /api/v1/plugin-host/invoke{ "result": ... }
WASMting_env.host_invoke + host_response_size + host_read_body读取到的 JSON 字节
Nativehost_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.getbooks_read 或 database_read查询当前用户可访问的书籍
libraries.list / libraries.getbooks_read 或 database_read查询当前用户可访问的存储库
chapters.list / chapters.getchapters_read 或 database_read查询章节元数据
progress.recentprogress_read 或 database_read读取当前用户最近播放进度
media.get_urlmedia_read_url 或 media_read获取受控播放地址
metadata.writemetadata_write创建元数据写入任务,需要管理员
library.file.list / stat / readfile_read在本地存储库根目录内读文件
library.file.writefile_write在本地存储库根目录内写文件,需要管理员
database.get / database.listdatabase_read受控实体读取,不是裸 SQL
database.updatedatabase_write受控实体更新,需要管理员
tasks.createtask_create创建插件自定义后台任务
cache.get / cache.hascache_read 或 cache_write读取插件隔离缓存
cache.set / cache.deletecache_write写入或删除插件隔离缓存
playlists.list / getplaylists_read 或 playlists_write查询当前用户的播放列表
playlists.create / update / deleteplaylists_write创建、更新或删除当前用户的播放列表
playlists.add_item / remove_itemplaylists_write向当前用户的播放列表追加或移除条目
favorites.listfavorites_read 或 favorites_write列出当前用户的收藏
favorites.add / removefavorites_write加入或移除当前用户的收藏
user_settings.getuser_settings_read 或 user_settings_write读取当前用户的设置
user_settings.setuser_settings_write写入当前用户的单个设置项

书籍、存储库和章节

books.list 支持 searchtaglibrary_idlimitoffsetlimit 默认 50,范围 1-200。返回分页结构:itemstotaloffsetlimit

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_ididlibraries.list 支持 limitoffset,管理员可以看到全部存储库,普通用户只能看到自己有权限访问的存储库。

chapters.list 必须传 book_idlimit 默认 200,最大 500;chapters.get 使用 chapter_idid,并会先检查当前用户是否能访问章节所属书籍。

{
  "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_ididtranscode 只支持 hlsmp3wavseek 会透传给流接口;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.listlibrary.file.stat 返回文件条目,字段包括 namepathis_fileis_dirsizemodified_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
  }
}

写入需要管理员上下文。参数可以传 textdata_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.getdatabase.listdatabase.update 只支持受控实体:book/bookschapter/chapterslibrary/librariesdatabase.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_scanwrite_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.0assistant-tools@1.0.1 是不同命名空间。cache.get 命中时返回 value 和时间戳,未命中返回 hit: falsevalue: 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_typebookseries),创建后自动插入。playlists.add_itemplaylists.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.setvalue 可为字符串、数字、布尔或对象,宿主会 JSON 编码后写入。保留字段 user_idupdated_atsettings_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 错误码含义
-1WASM 内存访问失败
-2字符串不是合法 UTF-8
-3参数不是合法 JSON
-8HostGateway 未配置
-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入参指针为空
-2Native HostGateway context 未激活
-3插件没有配置 HostGateway
-4当前调用缺少认证用户上下文
-5字符串或 JSON 参数解析失败
-6HostGateway 调用失败,详情在 result_json.error
-7响应 JSON 序列化或 CString 构造失败