HostGateway Reference

HostGateway is the unified entry point for plugins to access Ting Reader core data and controlled system capabilities. This page documents request envelopes, permissions, response shapes, and runtime bridge differences in the order plugin developers usually need them.

Invocation Entry Points and Response Envelopes

Capability registers a plugin capability and determines when the system calls the plugin. HostGateway lets a plugin access the host system and determines how it safely reads or operates on system data.

All runtimes call the same HostGateway method names with the same parameters. JavaScript backend methods receive the method result directly; the HTTP gateway wraps it in result; the Web container bridge returns a ting-plugin:response message.

ScenarioCall styleSuccessful result
JavaScript backend methodawait Ting.host.invoke(method, params)The HostGateway method result JSON
web_container UIpostMessage({ method: "host.invoke", params: { method, params } })The result field inside ting-plugin:response
HTTP gatewayPOST /api/v1/plugin-host/invoke{ "result": ... }
WASMting_env.host_invoke + host_response_size + host_read_bodyJSON bytes read from the response handle
Nativehost_invoke(method, params_json, result_json)The JSON string pointed to by result_json
POST /api/v1/plugin-host/invoke
Content-Type: application/json

{
  "plugin_id": "assistant-tools@1.0.0",
  "method": "progress.recent",
  "params": {
    "limit": 5
  }
}

The HTTP gateway wraps successful responses in result:

{
  "result": {
    "items": [],
    "total": 0,
    "offset": 0,
    "limit": 20
  }
}

The Web container bridge returns this shape. Plugin UI code should check ok before reading result:

{
  "type": "ting-plugin:response",
  "id": "request-id",
  "ok": true,
  "result": {
    "items": [],
    "total": 0,
    "offset": 0,
    "limit": 20
  }
}

HTTP errors use the backend's standard error structure, including an error type, human-readable message, and 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 and Native bridges do not return HTTP responses. When the host rejects a call, the JSON body usually contains an error field:

{
  "error": "Permission denied: Plugin assistant-tools@1.0.0 lacks permission required for host method books.list"
}

Methods and Permissions

Every HostGateway method checks permissions first, then checks the current user context. Initialization, public routes, or background paths without an authenticated user context will be rejected when reading books, progress, and library files.

MethodPermissionDescription
books.list / books.getbooks_read or database_readQuery books accessible to the current user
libraries.list / libraries.getbooks_read or database_readQuery libraries accessible to the current user
chapters.list / chapters.getchapters_read or database_readQuery chapter metadata
progress.recentprogress_read or database_readRead recent playback progress for the current user
media.get_urlmedia_read_url or media_readGet a controlled playback URL
metadata.writemetadata_writeCreate a metadata write task; admin required
library.file.list / stat / readfile_readRead files inside a local library root
library.file.writefile_writeWrite files inside a local library root; admin required
database.get / database.listdatabase_readControlled entity reads, not raw SQL
database.updatedatabase_writeControlled entity updates; admin required
tasks.createtask_createCreate custom plugin background tasks
cache.get / cache.hascache_read or cache_writeRead plugin-isolated cache
cache.set / cache.deletecache_writeWrite or delete plugin-isolated cache
playlists.list / getplaylists_read or playlists_writeQuery the current user's playlists
playlists.create / update / deleteplaylists_writeCreate, update, or delete the current user's playlists
playlists.add_item / remove_itemplaylists_writeAdd or remove items from the current user's playlists
favorites.listfavorites_read or favorites_writeList the current user's favorites
favorites.add / removefavorites_writeAdd or remove books from the current user's favorites
user_settings.getuser_settings_read or user_settings_writeRead the current user's settings
user_settings.setuser_settings_writeWrite a single setting for the current user

Books, Libraries, and Chapters

books.list accepts search, tag, library_id, limit, and offset. limit defaults to 50 and is clamped to 1-200. The response shape is paginated: items, total, offset, and 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 accepts book_id or id. libraries.list accepts limit and offset; admins can see all libraries, while regular users only see libraries they can access.

chapters.list requires book_id, defaults limit to 200, and clamps it to 500. chapters.get accepts chapter_id or id, then checks whether the current user can access the chapter's book.

{
  "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 and Media URLs

progress.recent reads the current user's recent playback records. limit defaults to 20 and is clamped to 1-100. Items include book, chapter, cover, position, and duration data.

{
  "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 returns a controlled playback URL. Pass chapter_id or id; transcode supports only hls, mp3, and wav; seek is forwarded to the stream endpoint; download: true generates a download 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 Access

Library file methods only target local libraries. Paths must be relative and cannot be absolute or escape the library root with ... library.file.read is limited to 20 MB, and library.file.write is limited to 50 MB.

library.file.list and library.file.stat return file entries with name, path, is_file, is_dir, size, and 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
  }
}

Writing requires an admin context. Pass either text or data_base64; existing files are not overwritten unless overwrite: true is set.

const written = await Ting.host.invoke("library.file.write", {
  library_id: "library-id",
  path: "三体/plugin-note.json",
  text: "{\"source\":\"plugin\"}",
  overwrite: true
});

Controlled Database and Metadata Writes

HostGateway does not expose raw SQL. database.get, database.list, and database.update support controlled entities only: book/books, chapter/chapters, and library/libraries; database.list also supports progress.

database.update requires an admin context and can update only whitelisted fields. It is useful for plugin tools that fix modeled fields such as title, author, cover, tags, or chapter title.

const updated = await Ting.host.invoke("database.update", {
  entity: "book",
  id: "book-id",
  patch: {
    title: "三体",
    author: "刘慈欣",
    tags: "科幻,中文"
  }
});

metadata.write does not write files directly. It creates a core metadata write task so the system queue, logs, and retry behavior are reused.

{
  "task_id": "task-id",
  "task_type": "write_metadata",
  "status": "queued",
  "book_id": "book-id"
}

Tasks and Cache

tasks.create can only create custom plugin tasks. library_scan and write_metadata are reserved core task types. The target task type must already be declared by a plugin task_handler.task_types entry.

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
}

Cache is isolated per plugin instance. assistant-tools@1.0.0 and assistant-tools@1.0.1 are separate namespaces. cache.get returns the value and timestamps on hit, or hit: false with value: null on miss.

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"
}

Personal Data: Playlists, Favorites, and User Settings

Playlist, favorite, and user settings methods are all bound to the current authenticated user. Plugins can only operate on the caller's own data; no admin context is required. Writes verify playlist.user_id == user.id and return PermissionDenied on mismatch.

playlists.create accepts an optional items array (item_type is book or series) that is inserted on creation. Use playlists.add_item and playlists.remove_item for incremental changes.

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 checks whether the current user can access the book first. Adding a duplicate returns { ok: true, created: false }. favorites.list returns { 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 accepts a value of string, number, boolean, or object; the host JSON-encodes it before storing. Reserved keys user_id, updated_at, and settings_json cannot be used. Without a key, user_settings.get returns all settings as a { key: value } map.

await Ting.host.invoke("user_settings.set", {
  key: "ai_booklist_prefers_narrator",
  value: "头陀渊讲故事"
});
const all = await Ting.host.invoke("user_settings.get", {});

Web Container Bridge

web_container UI pages cannot directly access backend objects. They call the host with postMessage. method: "host.invoke" calls HostGateway, while method: "capability.invoke" invokes the current or specified capability.

After the page loads, the host sends ting-plugin:init. The Web client currently sends plugin, capability, slot, and context fields; Flutter also includes optional theme data and sends ting-plugin:theme on theme changes.

{
  "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);
  }
});

External links can use ordinary <a target="_blank" rel="noopener noreferrer"> markup. The Web iframe allows popups, and Flutter forwards non-plugin http/https navigations, target="_blank", and window.open() to the system browser.

<a href="https://example.com/register" target="_blank" rel="noopener noreferrer">
  Register service
</a>

WASM and Native Bridges

WASM host_invoke returns a response handle. A positive value means JSON can be read; a negative value means a bridge-level error. After reading JSON, also treat an error field as a business failure.

#[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 codeMeaning
-1WASM memory access failed
-2String is not valid UTF-8
-3Params are not valid JSON
-8HostGateway is not configured
-9The current invocation has no authenticated user context
-10No Tokio runtime is active on the current thread
-11Host invocation thread panicked
-12Host response serialization failed

Native plugins receive the Host API through plugin_set_host_api. host_invoke returns 0 for success and a negative code for bridge or HostGateway failure. If result_json is non-null, read it and call host_free when done.

#[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 codeMeaning
-1A required pointer argument is null
-2Native HostGateway context is not active
-3HostGateway is not configured for the plugin
-4The current invocation has no authenticated user context
-5String or JSON parameter parsing failed
-6HostGateway call failed; details are in result_json.error
-7Response JSON serialization or CString construction failed