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.
| Scenario | Call style | Successful result |
|---|---|---|
| JavaScript backend method | await Ting.host.invoke(method, params) | The HostGateway method result JSON |
| web_container UI | postMessage({ method: "host.invoke", params: { method, params } }) | The result field inside ting-plugin:response |
| HTTP gateway | POST /api/v1/plugin-host/invoke | { "result": ... } |
| WASM | ting_env.host_invoke + host_response_size + host_read_body | JSON bytes read from the response handle |
| Native | host_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.
| Method | Permission | Description |
|---|---|---|
| books.list / books.get | books_read or database_read | Query books accessible to the current user |
| libraries.list / libraries.get | books_read or database_read | Query libraries accessible to the current user |
| chapters.list / chapters.get | chapters_read or database_read | Query chapter metadata |
| progress.recent | progress_read or database_read | Read recent playback progress for the current user |
| media.get_url | media_read_url or media_read | Get a controlled playback URL |
| metadata.write | metadata_write | Create a metadata write task; admin required |
| library.file.list / stat / read | file_read | Read files inside a local library root |
| library.file.write | file_write | Write files inside a local library root; admin required |
| database.get / database.list | database_read | Controlled entity reads, not raw SQL |
| database.update | database_write | Controlled entity updates; admin required |
| tasks.create | task_create | Create custom plugin background tasks |
| cache.get / cache.has | cache_read or cache_write | Read plugin-isolated cache |
| cache.set / cache.delete | cache_write | Write or delete plugin-isolated cache |
| playlists.list / get | playlists_read or playlists_write | Query the current user's playlists |
| playlists.create / update / delete | playlists_write | Create, update, or delete the current user's playlists |
| playlists.add_item / remove_item | playlists_write | Add or remove items from the current user's playlists |
| favorites.list | favorites_read or favorites_write | List the current user's favorites |
| favorites.add / remove | favorites_write | Add or remove books from the current user's favorites |
| user_settings.get | user_settings_read or user_settings_write | Read the current user's settings |
| user_settings.set | user_settings_write | Write 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 code | Meaning |
|---|---|
| -1 | WASM memory access failed |
| -2 | String is not valid UTF-8 |
| -3 | Params are not valid JSON |
| -8 | HostGateway is not configured |
| -9 | The current invocation has no authenticated user context |
| -10 | No Tokio runtime is active on the current thread |
| -11 | Host invocation thread panicked |
| -12 | Host 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 code | Meaning |
|---|---|
| -1 | A required pointer argument is null |
| -2 | Native HostGateway context is not active |
| -3 | HostGateway is not configured for the plugin |
| -4 | The current invocation has no authenticated user context |
| -5 | String or JSON parameter parsing failed |
| -6 | HostGateway call failed; details are in result_json.error |
| -7 | Response JSON serialization or CString construction failed |