V8 脚本
本页面向插件作者,讲解怎样用 V8 后端编写爬虫脚本:入口签名、配置怎么传入、宿主能力 Kabegame.*、运行时提供的 Web 平台全局,以及下载、分页与元数据。全部方法的完整签名见 Kabegame API 字典。
打包工具库 @kabegame/plugin-sdk 是另一回事——它是打包进插件产物的纯 JS 工具函数(resolveUrl、sleep、md5 等),与本页的运行时注入能力分开,见 plugin-sdk 工具库。
插件包结构与打包见 插件格式;两个后端的整体对比见 爬虫后端选择。
入口:crawl(common, custom)
V8 插件必须导出一个 crawl 函数。它可以是同步的,也可以返回 Promise;副作用全部通过 Kabegame.* 产生。
export async function crawl(common, custom) { const baseUrl = common.base_url ?? "https://example.com"; // custom 是用户在采集对话框填的 kbConfig 值 const page = custom.start ?? 1; await Kabegame.to(`${baseUrl}/posts?page=${page}`); // ...}common —— 宿主提供的公共配置
当前只有一个键:
| 字段 | 类型 | 说明 |
|---|---|---|
common.base_url | string | null | 来自 package.json 的 kbBaseUrl(trim 后;未定义为 null)。 |
custom —— 用户填的表单值
custom 是 kbConfig 所有变量的合并结果(默认值叠加用户输入),形如 { [key]: value }。每个值的类型对应其 kbConfig 声明(int/float → 数字,string → 字符串,boolean → 布尔,options → 选中项的 variable,list → 字符串数组,checkbox → { variable: bool } 对象……)。变量类型详见 插件清单字段 · kbConfig。
export async function crawl(_common, custom) { // 不需要 base_url 时用 _common 忽略第一参 const mode = custom.crawl_mode; // options → "all" | "tags" | ... const tags = custom.mode_tag_value ?? []; // list → string[] const includeR18 = custom.r18 === true; // boolean}运行时全局
除了 Kabegame,V8 运行时还提供一批标准 Web 平台全局,直接使用即可,无需 import:
URL/URLSearchParamsTextEncoder/TextDecoderatob/btoacrypto/crypto.subtlefetch/Request/Response/HeaderssetTimeout/setInterval/clearTimeout/clearIntervalDOMParser(把 HTML 字符串解析成 DOM,用于选择器查询)console.*
用 fetch 请求 JSON
旧的 @kabegame/plugin-sdk/host 与 fetchJson 已移除。请求 JSON 用标准 fetch:
const data = await (await fetch(url)).json();fetch 会合并当前任务通过 Kabegame.setHeader() 设置的请求头,但不会按当前页面自动解析相对 URL。需要相对路径时显式写:
const abs = new URL(relative, await Kabegame.currentUrl()).href;Kabegame.* 宿主能力
宿主对象 Kabegame 在每个 V8 插件里全局可用(不从 SDK 导入)。下面按用途分组,完整签名与返回值见 Kabegame API 字典。
页面栈与导航
运行时维护一个页面栈:
await Kabegame.to(url)— 访问一个页面并推入栈,返回最终 URL(相对 URL 相对当前页解析)。await Kabegame.back()— 弹出栈顶页面。await Kabegame.currentUrl()/currentHtml()/currentDocument()/currentHeaders()— 读栈顶页面的 URL / 原始 HTML / 解析后的Document/ 响应头。
await Kabegame.to(postUrl);const doc = await Kabegame.currentDocument();const href = doc?.querySelector(".icon-download")?.getAttribute("href");if (href) { await Kabegame.downloadImage(new URL(href, await Kabegame.currentUrl()).href);}await Kabegame.back();下载图片 / 视频
await Kabegame.downloadImage(imageUrl, { name: "artist / character", // 展示名 / 文件名 url: postUrl, // 来源 URL metadata: { schema: 1, title: "Post title", tags: ["wallpaper"] }, searchTexts: ["Post title", "my-plugin_tags_wallpaper"],});downloadImage把资源加入下载队列。opts.metadata是任意 JSON,写入metadata行,用于详情页展示(配合templates/description.ejs)。存储时会由应用自动盖上运行插件的版本号;请在 metadata 里自带一个schema标记供迁移脚本判断。- 多张图共享一行 metadata,或需要在解析出图片 URL 前先写 metadata 时,用
createImageMetadata拿到metadata_id再传给downloadImage(url, { metadata_id })。
元数据搜索片段
downloadImage(url, { metadata, searchTexts }) 与 createImageMetadata(value, { searchTexts }) 可以为新 metadata 提供精选搜索片段。缺省时应用按键名与字符串叶子展开 JSON;空数组表示不参与元数据搜索。downloadImage 同时传 metadata_id 时不会新建 metadata,因此会忽略 searchTexts 并写警告。
- 尽量覆盖
description.ejs展示给用户的原文,例如标签显示名,让搜索符合直觉。 - 可以增加
<插件>_<字段>_<值>形式的前缀条目,支持my-plugin_rating_safe这类按字段筛选。 - 不要收录键名、页面看不到的链接,以及会过期或没有信息量的文字。
- 每项最多 1024 字符、最多 1000 项;片段会清理换行与首尾空白并去重,无效项记录警告后跳过。
图片标签
downloadImage 可带 labels,图片入库成功后会出现在应用的「标签」分区里(标签就是一种画册),用户可按标签浏览和搜索:
await Kabegame.downloadImage(imageUrl, { url: postUrl, labels: [ { key: "hatsune_miku", category: "pixiv/character", name: "初音ミク" }, { key: "vocaloid" }, // category 缺省为插件 id → 挂在「<插件 id>」下 ],});key必填,只能是[a-zA-Z0-9_-]、英文括号()和空格(空格不能在首尾或连续),不超过 64 字节;同一级下不区分大小写唯一。category各段同规则。category是用/分隔的上级 key 链(每段同样的字符集),缺省为插件 id;缺失的各段会自动建成标签目录,末端key建成可挂图的标签。name只在首次创建该标签时作为显示名,缺省等于key;已存在的标签永远不会被插件改名(用户可能改过)。- 不合规的标签,或
category段与已有标签同名、末端与已有标签目录同名时,会写一条任务日志警告并只跳过该标签;同批其它标签和图片下载不受影响。输入不会被自动修正;labels不是数组时整次调用报参数错误。 - 标签只增不减。命中去重时,只有开启「去重时更新元数据」才会给已有图片补标签。
- 只挂叶子标签:上级由
category隐含,搜索时勾选「包含子标签」即可按目录搜。
请求头与 Cookie
Kabegame.setHeader("Cookie", "sitelang=zh-cn"); // 影响后续 to() 与 fetchKabegame.delHeader("Cookie");
// 让宿主把用户在「畅游」里登录该站的 cookie 注入本任务请求头:if (!Kabegame.requireCookie()) { // 省略 host 时从 base_url 推断 Kabegame.warn("请先在畅游里登录该站点");}Kabegame.requireCookie(host?) 4.4.0 不会把 cookie 明文暴露给插件,返回值只表示注入是否成功。需要这个能力时把 engines.kabegame 抬到 >=4.4.0。配合 kbLabels 的 auth.needCookie 标签向用户提示。
进度与日志
Kabegame.addProgress(100 / imageUrls.length); // 累加进度,宿主会 clampKabegame.warn("排行榜实际获取数量少于请求上限"); // warn 级任务日志console.log("普通日志"); // 走 console插件私有数据
Kabegame.pluginData() / setPluginData(map) 读写按插件 ID 隔离的持久 JSON 对象,适合缓存游标、token、TTL 状态等:
const data = Kabegame.pluginData<{ cursor?: string }>();if (data.cursor) await Kabegame.to(data.cursor);// ...Kabegame.setPluginData({ cursor: nextPageUrl });私有虚拟文件系统 Kabegame.fs
每个 V8 爬虫任务有一个私有虚拟文件系统,暴露完整的 deno_fs 接口(含同步方法)。路径从 Kabegame.fs.getRoot()(V8 下同步返回)开始,句柄随任务结束失效,不要持久化虚拟路径。
const root = Kabegame.fs.getRoot();await Kabegame.fs.writeTextFile(`${root}/data/cursor.txt`, "abc");虚拟路径媒体工具 Kabegame.ffmpeg
Kabegame.ffmpeg 4.4.0 提供两个高层媒体函数:
await Kabegame.ffmpeg.muxStreams(inputs, output):对至少两个输入流做 stream-copy 合流。await Kabegame.ffmpeg.probe(path):返回{ isVideo, mimeType, width, height, browserSafe },无法识别或非视频时返回null。
输入与输出都必须是当前任务 VFS 的虚拟绝对路径。插件使用这些 API 时须声明 engines.kabegame >= 4.4.0;Android 调用会抛出可读的不支持错误。
下面示例把已获取的音视频分片写入 VFS、合流,再用同一个虚拟路径下载 API 提交入库。文件句柄的 write() 可能短写,因此循环到整段写完:
async function writeParts(path: string, parts: ArrayBuffer[]) { const file = await Kabegame.fs.create(path); try { for (const part of parts) { const bytes = new Uint8Array(part); let offset = 0; while (offset < bytes.byteLength) { const written = await file.write(bytes.subarray(offset)); if (written <= 0) throw new Error("写入媒体分片失败"); offset += written; } } } finally { file.close(); }}
const root = Kabegame.fs.getRoot();const workDir = `${root}/tmp/media-example`;const videoPath = `${workDir}/video.mp4`;const audioPath = `${workDir}/audio.mp4`;const outputPath = `${workDir}/output.mp4`;
await Kabegame.fs.mkdir(workDir, { recursive: true });try { await writeParts(videoPath, videoFragments); await writeParts(audioPath, audioFragments); await Kabegame.ffmpeg.muxStreams([videoPath, audioPath], outputPath); await Kabegame.downloadImage(outputPath, { name: "合流视频", url: postUrl });} finally { await Kabegame.fs.remove(workDir, { recursive: true });}提前退出与取消
crawl正常返回即结束;抛异常则任务失败并记录错误。- 需要提前结束就直接
return。 - 任务被用户取消后,下一次
Kabegame.*调用会抛错;用await让异常自然向外传播即可干净中止。
元数据迁移
插件升级后,历史图片的 metadata 结构可能变化。提供 metadata_migrations/migrate.js 并在 package.json 声明 kbMetadataMigration,即可把旧行就地迁移到新结构。
契约:一个 ES module,export function migrate(metadata) 的入参与返回值都是 JSON 字符串,运行在裸 V8(无 import、无 Kabegame.*、无宿主 API,只有原生 JSON/String/RegExp 等)。脚本应按 metadata 里的 schema 自检、幂等、一步到位。
export function migrate(json) { const m = JSON.parse(json); if (m.schema === 1) return json; // 已是最新结构,原样返回 // ...把旧结构升级到 schema 1... return JSON.stringify({ schema: 1, title: m.title });}迁移后的行按 (plugin_id, plugin_version, data) 去重;迁移成功会先发出带最终 metadataId / pluginVersion 的
image-changed 字段快照,再发出作用域为该插件的 images-change(change) 对账列表。
每一行无论迁移成功与否都只处理一次:失败时保留原数据并同样盖上当前插件版本,不会在每次启动时反复重试;修好脚本后需要再发一个新版本。
给历史图片补标签
同一个脚本可以再导出 provideLabels(json)(允许 async),返回与 downloadImage 的 labels 相同结构的数组。应用只在 migrate 成功后,用迁移后的 metadata 调用它,并把标签挂到引用这行 metadata 的所有图片上。
export function provideLabels(json) { const m = JSON.parse(json); return (m.characters ?? []).map((c) => ({ key: c.slug, category: "my-plugin/character", name: c.name }));}给历史图片提供搜索片段
脚本还可导出 provideSearchTextList(json)(允许 async),返回字符串数组。调用失败或未导出时回退到默认 JSON 展开;显式返回空数组表示不参与元数据搜索。编写原则与爬取时的 searchTexts 相同:覆盖模板展示原文,可增加字段前缀,不收录键名、不可见链接或过期文字。
export function provideSearchTextList(json) { const m = JSON.parse(json); return [m.title, ...(m.tags ?? []).map((tag) => `my-plugin_tags_${tag}`)].filter(Boolean);}migrate、provideLabels 与 provideSearchTextList 至少导出一个;没有 migrate 时,两个 provider 都按原数据调用。
延伸阅读
- Kabegame API 字典 ——
Kabegame.*每个方法的完整签名(V8 / WebView 对照)。 - plugin-sdk 工具库 —— 打包进插件的纯工具函数。
- WebView 脚本 —— 真实浏览器后端的写法。
- 插件格式 ——
package.json与.kgpg结构。 - 插件清单字段 ——
kbConfig/kbLabels全字段。