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"] },});downloadImage把资源加入下载队列。opts.metadata是任意 JSON,写入image_metadata行,用于详情页展示(配合templates/description.ejs)。存储时会由应用自动盖上运行插件的版本号;请在 metadata 里自带一个schema标记供迁移脚本判断。- 多张图共享一行 metadata,或需要在解析出图片 URL 前先写 metadata 时,用
createImageMetadata拿到metadata_id再传给downloadImage(url, { metadata_id })。
请求头与 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)(或 export default),入参与返回值都是 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, version, content_hash) 去重;迁移成功会发出作用域为该插件的 metadata-migrate 变更事件刷新详情缓存。
延伸阅读
- Kabegame API 字典 ——
Kabegame.*每个方法的完整签名(V8 / WebView 对照)。 - plugin-sdk 工具库 —— 打包进插件的纯工具函数。
- WebView 脚本 —— 真实浏览器后端的写法。
- 插件格式 ——
package.json与.kgpg结构。 - 插件清单字段 ——
kbConfig/kbLabels全字段。