跳转到内容

JavaScript 插件

本规范说明为 Mankai 提供在线漫画内容的插件所使用的 JSON 格式和 JavaScript API。

JavaScript 插件是只读内容源。应用从网址或粘贴的 JSON 加载清单,然后在隐藏的 WebKit 网页视图中执行回调脚本。

清单是一个 JSON 对象。解码器只要求提供 id 字段,但仍需提供实际使用的操作所要求的回调脚本。

type ScriptName =
| 'isOnline'
| 'getSuggestion'
| 'search'
| 'getList'
| 'getMangas'
| 'getMangaUpdates'
| 'getDetailedManga'
| 'getChapter'
| 'getImage'
type PluginCapability =
| 'onlineCheck'
| 'suggestions'
| 'list'
| 'listByGenre'
| 'listByStatus'
| 'search'
| 'searchByGenre'
| 'searchByStatus'
| 'searchByAuthor'
| 'mangaDetails'
| 'batchMangas'
| 'mangaUpdates'
| 'chapter'
| 'image'
interface JsPluginManifest {
id: string
name?: string
version?: string
description?: string
authors?: string[]
repository?: string
updatesUrl?: string
availableGenres?: Genre[]
scripts?: Record<ScriptName, string>
configs?: Config[]
getImageHeaders?: Record<string, string>
cooldown?: Cooldown
capabilities?: PluginCapability[]
}

字段 类型 说明
id string 稳定且全局唯一的插件标识符,也用于划分持久存储的作用域。
name string 显示名称,省略时使用 ID。
version string 在应用中显示并用于检查更新的插件版本。
description string 在来源设置中显示的简短描述。
authors string[] 插件作者,默认值为 []。
repository string 源代码或项目的网址。
updatesUrl string 用于检查更新的清单网址。
availableGenres Genre[] 内容源支持的漫画类型,默认值为 []。
scripts Record<string, string> 以以下脚本名称为键的 JavaScript 源代码,未知的键会被忽略。
configs Config[] 向脚本提供的用户可配置值,默认值为 []。
getImageHeaders Record<string, string> 如果存在,Mankai 使用这些原生请求标头下载所有图片网址,而不调用 getImage 脚本。
cooldown Cooldown 可选的请求节流和图片并发限制。
capabilities PluginCapability[] 插件支持的操作,默认启用除 mangaUpdates 以外的所有能力。

每个脚本值都是包含函数和导出标记的字符串。

async function isOnline() {
const response = await fetch('https://example.com/')
return response.ok
}
export{isOnline as default};

标记必须使用 export{functionName as default}; 的形式。Mankai 在执行前移除该标记并调用指定函数。函数可以是同步或异步函数,Mankai 会等待返回结果。除了导出的函数,脚本还可以包含辅助函数。

可选字段 capabilities 接受下列值。

onlineCheck, suggestions, list, listByGenre, listByStatus, search, searchByGenre, searchByStatus, searchByAuthor, mangaDetails, batchMangas, mangaUpdates, chapter, image

键与函数签名如下。

键 函数签名 返回值
isOnline isOnline() boolean
getSuggestion getSuggestion(query) string[]
search search(query, page, genre, status, isAuthor) Manga[]
getList getList(page, genre, status) Manga[]
getMangas getMangas(ids) Manga[]
getMangaUpdates getMangaUpdates(mangas) MangaUpdate[] 元数据补丁与更新标记
getDetailedManga getDetailedManga(id) DetailedManga
getChapter getChapter(manga, chapter) 图片网址数组 string[]
getImage getImage(url) Base64 图片数据或图片代理请求对象

以下说明各参数与返回结果。

当内容源可访问且可用时返回 true,否则返回 false。抛出异常或返回非布尔值会被视为插件调用失败。

返回适合用作搜索建议的字符串数组。清单中的键使用单数形式 getSuggestion。

async function getSuggestion(query) {
const response = await fetch(
`https://example.com/suggestions?q=${encodeURIComponent(query)}`,
)
if (!response.ok) return []
return await response.json()
}
export{getSuggestion as default};

search(query, page, genre, status, isAuthor)

章节「search(query, page, genre, status, isAuthor)」

按标题或作者搜索漫画。page 是从 1 开始的页码。genre 是漫画类型中的一个字符串,status 是状态中的一个数值。当 isAuthor 为 true 时,搜索内容源的作者字段而不是标题字段。返回轻量的 Manga 对象数组。

getList(page, genre, status)

章节「getList(page, genre, status)」

返回内容源的分页列表,使用与 search 相同的 page、genre 和 status 参数。

根据请求的字符串 ID 返回轻量漫画对象。如果内容源已不再包含某些 ID,返回的条目数可以少于请求数。

每个请求条目包含漫画 ID 和 Mankai 当前已知的最新章节。此回调让插件控制更新行为。如果希望使用 Mankai 默认的批量比较方式,请省略 mangaUpdates 能力。

interface MangaUpdateRequest {
id: string
latestChapter: Chapter
}

返回 MangaUpdate[]。每个结果必须包含 id 和 updates。Mankai 仅根据 updates 的值决定是否将已保存的漫画标记为有更新。Mankai 会根据所有返回结果刷新元数据,包括 updates 为 false 的结果。

interface MangaUpdate extends Manga {
updates: boolean
}
async function getMangaUpdates(mangas) {
const response = await fetch('https://example.com/manga/updates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mangas),
})
if (!response.ok) return []
return await response.json()
}
export{getMangaUpdates as default};

例如,插件可以返回 { id: "completed-series", updates: false, status: 2 },只刷新状态而不显示未读更新。

返回完整的 DetailedManga 对象。Mankai 会先将返回值序列化为 JSON 再解码,因此应返回 JavaScript 对象,而不是 JSON 字符串。

getChapter(manga, chapter)

章节「getChapter(manga, chapter)」

按顺序返回 chapter 的图片网址。manga 参数是 getDetailedManga 返回的完整对象,chapter 是所选章节对象。图片网址应为绝对网址,因为 Mankai 随后会逐一将其传入 getImage。

async function getChapter(manga, chapter) {
const response = await fetch(
`https://example.com/manga/${encodeURIComponent(manga.id)}/chapter/${encodeURIComponent(chapter.id)}`,
)
if (!response.ok) return []
return await response.json()
}
export{getChapter as default};

未提供 getImageHeaders 时,Mankai 对封面和章节页面调用此脚本。接受以下两种结果。

  1. 包含原始图片字节的 Base64 字符串,不要包含 data:image/...;base64, 前缀。

  2. 指示 Mankai 发起原生请求的对象。

    interface ImageProxyRequest {
    url: string
    headers: Record<string, string>
    }

例如:

async function getImage(url) {
return {
url,
headers: {
Referer: 'https://example.com/',
'User-Agent': 'Mozilla/5.0',
},
}
}
export{getImage as default};

如果清单包含 getImageHeaders,系统会自动对每个图片网址发起等效的原生请求。

{
"getImageHeaders": {
"Referer": "https://example.com/",
"User-Agent": "Mozilla/5.0"
}
}

回调结果接受以下结构,可选属性可以省略。

interface Chapter {
id: string
title?: string
locked?: boolean
}
interface Manga {
id: string
title?: string
cover?: string
status?: Status
latestChapter?: Chapter
meta?: string
}
interface ChapterGroup {
id?: string
title: string
chapters: Chapter[]
}
interface DetailedManga {
id: string
title?: string
cover?: string
status?: Status
readingDirection?: ReadingDirection
latestChapter?: Chapter
description?: string
externalLink?: string
updatedAt?: number // Unix timestamp in milliseconds
authors?: string[]
genres?: Genre[]
chapters?: ChapterGroup[]
remarks?: string
meta?: string
}

Mankai 识别以下字符串值。

all, action, romance, yuri, boysLove, otokonoko, schoolLife, adventure,
harem, speculativeFiction, war, suspense, fanFiction, comedy, magic, horror,
historical, sports, mature, mecha

名称 值
any 0
onGoing 1
completed 2

名称 值
leftToRight 1
rightToLeft 2
vertical 3

Mankai 在每个回调脚本中注入以下辅助函数。

运行环境提供使用原生网络的 fetch 实现,支持以下请求选项。

interface FetchOptions {
method?: string // Defaults to "GET"
headers?: Record<string, string> | Headers
body?: string
}

返回的响应具有以下属性和方法。

interface FetchResponse {
ok: boolean
status: number
statusText: string
url: string
headers: Headers
text(): Promise<string>
json(): Promise<any>
blob(): Promise<Blob>
arrayBuffer(): Promise<ArrayBuffer>
}

console.log 会重定向至 Mankai 的 JavaScript 插件日志,可用于开发期间诊断插件。

console.log('searching', query)

这些异步辅助函数使用 OpenCC 转换中文文本。

const traditional = await s2t('简体转繁体')
const simplified = await t2s('繁體轉簡體')

s2t 将简体中文转换为繁体中文,t2s 执行反向转换。

以数组形式返回清单所声明的配置项的当前值。

interface ConfigValue {
key: string
value: unknown
}

示例:

function configValue(key) {
return getConfigs().find((config) => config.key === key)?.value
}
const username = configValue('username')

这些辅助函数在以插件 id 为作用域的键值存储中保存字符串值。

getValue(key: string): Promise<string | null>;
setValue(key: string, value: string): Promise<void>;
removeValue(key: string): Promise<boolean>;

三个辅助函数都应使用 await。应用重启和插件更新后,值仍然保留。删除插件时,这些值也会删除。

const token = await getValue('token')
if (!token) {
await setValue('token', 'new-token')
}
await removeValue('temporary-value')

配置项采用以下结构。

interface Config {
key: string
name: string
description?: string
type:
'text' | 'password' | 'number' | 'slider' | 'boolean' | 'select' | 'color'
defaultValue: unknown
options?: string[]
min?: number
max?: number
step?: number
supportsOpacity?: boolean
}

options 用于 select 配置。min、max 和 step 指定 slider 配置的范围和步长。应用使用 defaultValue 初始化每个配置项,持久保存用户在来源设置中的修改,并通过 getConfigs() 提供当前值。

示例:

{
"configs": [
{
"key": "language",
"name": "Language",
"description": "Language used when requesting titles.",
"type": "select",
"defaultValue": "en",
"options": ["en", "zh-Hans", "zh-Hant"]
},
{
"key": "imageQuality",
"name": "Image quality",
"type": "slider",
"defaultValue": 80,
"min": 10,
"max": 100,
"step": 5
},
{
"key": "includeMature",
"name": "Include mature content",
"type": "boolean",
"defaultValue": false
}
]
}

冷却时间以毫秒为单位。

interface Cooldown {
default?: number
getImage?: number
getImageConcurrency?: number
}
字段 说明
default 搜索、列表、详情和章节请求等非图片操作之间的最小间隔。
getImage 图片请求之间的最小间隔,图片与其他操作使用独立的调度。
getImageConcurrency 同时进行的图片请求数量上限,小于 1 的值表示不设置限制。

使用这些字段遵守内容源的请求频率限制,例如:

{
"cooldown": {
"default": 500,
"getImage": 100,
"getImageConcurrency": 2
}
}