在线咨询
Skip to content

钩子事件 ​

钩子事件让微应用不需要界面就能介入迅雷的下载流程:当用户创建的任务命中声明的匹配规则时,迅雷自动执行对应脚本,完成解析、改写、恢复等操作。

典型的用法是 onResolve:用户输入下载链接后,脚本请求该页面、解析出真实的文件地址,迅雷直接下载——用户全程无感。

下载任务生命周期:用户输入下载链接后触发 onResolve,创建任务后触发 onStart,下载完成调用 onDone,发生错误调用 onError

快速示例 ​

1. 在 manifest 中声明事件脚本:

json
{
  "permissions": ["network"],
  "network": {
    "urls": ["*://example.com/*"]
  },
  "scripts": [
    {
      "event": "onResolve",
      "match": {
        "urls": ["*://example.com/*"]
      },
      "entry": "events/onResolve.js"
    }
  ]
}

事件脚本中发起的网络请求同样受 network.urls 白名单约束,声明里一并配置。

2. 编写脚本(src/events/onResolve.ts,构建后输出为 events/onResolve.js):

ts
xunlei.events.onResolve(async (ctx) => {
  // 请求目标页面
  const response = await fetch(ctx.req.url);
  const html = await response.text();

  // 解析出真实下载地址
  const matches = html.matchAll(/href="(https:\/\/[^"]+\.zip)"/g);
  const files = Array.from(matches, (m) => ({
    name: m[1].split('/').pop()!,
    req: { url: m[1] },
  }));

  if (files.length > 0) {
    ctx.res = {
      name: '解析结果',
      files,
    };
  }
});

脚本加载后会立即执行,此时必须同步完成回调注册;事件触发时迅雷调用已注册的 handler 并传入 ctx。

不要在 UI 页面注册

事件回调只能写在 scripts[].entry 指向的脚本中,与 UI 页面无关(页面没打开也会执行)。UI 页面中注册的回调不会被投递。

事件脚本运行环境 ​

事件脚本运行在一个独立的 JS 沙箱中,特点是:

能力说明
基础对象console、setTimeout / setInterval、URL、eval、Function 等
网络访问fetch / XMLHttpRequest,受 network.urls 白名单约束
平台 APIxunlei.info / logger / settings / storage / events / tasks / runtime / host.env
❌ DOM无页面渲染能力(document 不可用)
❌ 导航不支持 window.open、页面跳转

事件脚本也能调用完整的 xunlei.tasks,例如查询或创建任务;调用时须在 manifest 中声明对应的具体权限。onStart / onError 上下文提供的请求修改与恢复方法不受这些权限控制。

脚本引擎生命周期由宿主管理:脚本运行环境的创建与销毁完全由迅雷控制——事件触发时启动(执行脚本、注册回调、执行 handler),完成后自动回收;下次事件触发时重新启动,因此不要依赖全局变量、模块级缓存等在多次触发之间传递状态。需要持久化或与页面运行时共享的数据,请写入 xunlei.storage。脚本也不应依赖(或试图干预)运行环境的存续,清理与回收由宿主自动完成。

超时限制:单次事件处理总超时约 60 秒,超时以 EVENT_TIMEOUT 失败,脚本内不要做长时间阻塞操作。

四个生命周期事件 ​

onResolve — 解析任务 ​

触发时机:用户提交下载请求后、创建任务前。

脚本根据 ctx.req(用户请求,只读)解析页面,把结果写入 ctx.res:

ts
interface OnResolveContext {
  req: Request; // 只读:{ url, extra?, labels? }
  res?: OnResolveResource; // 脚本写入解析结果
}

interface OnResolveResource {
  name?: string; // 任务显示名称,可省略
  size?: number; // 总大小(字节),可省略
  files: OnResolveFileInfo[];
}

interface OnResolveFileInfo {
  name?: string; // 文件名,可省略
  path?: string; // 文件相对路径,可省略
  size?: number; // 文件大小,可省略
  req: Request; // 该文件的下载请求,必填(可带 header)
}

解析结果中只有每个文件的 req 是必填项——只知道下载地址时,文件项可以仅写 { req: { url } };显式提供文件名与 path 时,注意处理非法字符与越界路径。

宿主行为:

  • ctx.res 包含有效 files 时,每个 file.req(含 URL、请求头等)交给下载器执行下载,res.name 作为任务名;
  • ctx.res 未赋值时初始为 undefined,视为该微应用不处理此 URL,宿主跳过它继续尝试其它微应用。

onStart — 任务开始前 ​

触发时机:任务已创建、即将开始下载。可在此修改任务的请求地址、请求头与标签:

ts
xunlei.events.onStart(async (ctx) => {
  // 覆盖下载地址(如换成解析出的直链)
  await ctx.task.meta.req.setUrl('https://cdn.example.com/real-file.zip');

  // 全量替换 HTTP 请求头(仅 HTTP 任务)
  await ctx.task.meta.req.setHeaders({ Referer: 'https://example.com/watch/123' });

  // 打标签(可被同微应用的其它事件通过 match.labels 匹配)
  await ctx.task.meta.req.putLabel('source', 'miniapp');
});

ctx.task.meta.req 提供的修改方法:

方法说明
setUrl(url)覆盖任务请求 URL
setHeaders(headers)全量替换请求头,空对象表示清空(仅 HTTP 任务)
putHeader(name, value)添加或替换单个请求头(仅 HTTP 任务)
delHeader(name)删除单个请求头(仅 HTTP 任务)
setLabels(labels)全量覆盖任务请求标签
putLabel(key, value)设置单个标签
delLabel(key)删除单个标签

控制方法返回 Promise,依赖执行结果时请 await。补充说明:

  • 请求头名称原样交给宿主适配;运行时不限制名称、不会统一大小写。实际支持范围由宿主决定;
  • setHeaders 为全量替换,遗漏的请求头即被清空,增量调整用 putHeader / delHeader;
  • 非 HTTP 任务调用请求头方法无效果;setHeaders 参数须为对象,字段值会转为字符串;
  • 任务事件中的请求 URL 与标签是只读的,只能通过上述方法修改,不能直接赋值;ctx.task.meta.req.extra 不属于公开读取 API,修改请求头后也不要从中读取结果;
  • 修改方法成功后,当前请求的 URL / 标签快照会更新;调用失败则抛错,保留原值。

onError — 任务出错 ​

触发时机:任务执行出错(网络错误、下载失败等)。ctx.task 可能是普通任务,也可能是任务组,通过 type 字段区分。

ts
xunlei.events.onError(async (ctx) => {
  // ctx.error(只读)为错误详情
  if (ctx.error.message.includes('403')) {
    // token 失效:刷新凭证、修正请求头后恢复任务
    const cookie = await refreshToken();

    // 任务组时 children 为命中该脚本的子任务,可逐个处理
    const tasks = ctx.task.type === 'group' ? ctx.task.children : [ctx.task];
    for (const task of tasks) {
      await task.meta.req.setHeaders({ Cookie: cookie });
      await task.continue(); // 恢复任务重新下载
    }
  }
  // 不做任何操作则按默认错误处理(任务标记为 error)
});
字段 / 方法说明
ctx.task普通任务或任务组;组没有 meta.req,子任务才有请求修改方法
ctx.error错误详情,只读
ctx.task.continue()恢复任务重新下载;任务组上调用则恢复整组,也可遍历 children 单独恢复

任务组的 children 只包含命中当前脚本的子任务;同一微应用的同一脚本入口会合并处理这些子任务。组的 id、状态及汇总信息仍指整个任务组。任务组结束时只触发一次 onError / onDone,不会为每个子任务单独触发;onStart 则按子任务逐个触发。

onDone — 任务完成 ​

触发时机:任务所有文件下载完成。ctx.task 可能是普通任务或任务组,只读(无任何控制方法),典型用法是通知外部服务:

ts
xunlei.events.onDone(async (ctx) => {
  await fetch('https://example.com/webhook', {
    method: 'POST',
    body: JSON.stringify({
      event: 'download_complete',
      taskId: ctx.task.id,
      name: ctx.task.name,
    }),
  });
});

任务组完成时同样触发一次,ctx.task.type === 'group',需要子任务明细时遍历 children。

URL 匹配规则 ​

匹配规则分三类,从粗到细:

规则含义示例
<all_urls>全部链接都匹配所有链接
协议:*只按协议过滤,不解析后续内容http:*、ed2k:*、magnet:*
协议://主机/路径按协议、主机和路径分别过滤https://*.example.com/api/*、ws://*.example.com/api/*

<all_urls>(匹配全部) ​

特殊规则 <all_urls> 对任意请求地址直接命中,包括磁力链接等非 HTTP 协议,也包括空字符串和非标准 URL;下载链接是否有效仍由宿主判断:

json
{
  "scripts": [
    {
      "event": "onResolve",
      "match": { "urls": ["<all_urls>"] },
      "entry": "events/onResolve.js"
    }
  ]
}

使用约束:

  • <all_urls> 必须单独作为完整规则书写,不能与其它段组合或拼接路径(如 <all_urls>/foo 属于非法写法);
  • 它可以和普通规则一起放在 urls 数组中;只要包含 <all_urls>,URL 条件就直接命中。此规则仅用于 scripts[].match.urls,写在 network.urls 中不会放行网络请求;
  • 匹配所有请求意味着每个下载都会运行脚本,脚本自身应尽快判断是否处理(如检查 ctx.req.url),避免无意义的解析开销与超时风险。

协议级规则(协议:*) ​

写作 协议:*,只按协议过滤、不解析后续内容,没有主机与路径概念,命中该协议下的全部地址:

写法匹配
magnet:*全部磁力链接
ed2k:*全部电驴链接
https:* / http:*对应协议的全部地址
ws:* / wss:*对应 WebSocket 协议的全部地址

协议名以英文字母开头,后续只能是英文字母或数字,匹配时不区分大小写。规则只比较链接开头冒号前的协议名,不校验冒号后的内容,因此 ed2k:* 可命中 ed2k://|file|...。不支持 *:*、magnet:*suffix 等写法;协议:* 仅用于事件匹配,写在 network.urls 中不会放行网络请求。

完整结构(协议://主机/路径) ​

构成为:

text
<scheme>://<host>/<path>?<query>#<fragment>

scheme、host 必须书写,path 可省略(如 https://* 表示该协议下任意非空主机和路径);?query 与 #fragment 可选。匹配时各段独立比对,全部命中才算匹配成功。

scheme(协议) ​

协议段支持:

写法匹配
http / https / ws / wss精确匹配对应协议
自定义协议以英文字母开头,后续为字母或数字,如 abc、ed2k
*任意协议,包括 HTTP(S)、WS(S) 和自定义协议

*:// 仍要求链接符合主机/路径结构;例如 *://*/* 能匹配 abc://sss,但不匹配没有有效主机结构的 ed2k://|file|...,后者可用 ed2k:* 或 <all_urls>。

host(主机) ​

写法匹配
www.example.com精确主机
*.example.comexample.com 及其任意子域名(裸域本身也命中)
*任意非空主机
  • * 只能出现在 host 开头且紧跟 .,*example.com、www.*.com 均为非法写法;
  • 端口不参与匹配:http://localhost/* 同样命中 http://localhost:3000/dev。

path(路径) ​

  • * 可出现在任意位置,匹配零个或多个路径字符(不会跨过 ? 与 #);
  • 以 /* 结尾时同时命中目录本身:/video/* 既命中 /video/1,也命中 /video;
  • 结尾的 / 视为可选:/video/ 与 /video 互相匹配。

query 与 fragment(可选段) ​

声明匹配行为
不写 ? / # 部分匹配任意查询串与锚点
?type=video要求查询串中存在 type=video,允许其它参数并存
?type=*要求存在 type 参数,值任意
?!type=video(! 严格模式)查询串只允许包含声明的参数
text
https://example.com/list?type=video    命中  /list?type=video&page=2(存在即可,允许并存)
https://example.com/list?!type=video   未命中 /list?type=video&page=2(page=2 未声明)

示例 ​

Pattern命中不命中
*://github.com/*https://github.com/xunlei/demohttps://gitlab.com/anything
https://*.example.com/video/*https://cdn.example.com/video/1、https://example.com/video/1、https://example.com/videohttps://example.com/image/1
http://localhost/*http://localhost:3000/devhttps://localhost/dev
magnet:*任意磁力链接(magnet:?xt=urn:btih:...)https://example.com/file.zip
https:*任意 HTTPS 地址http://example.com/file.zip
*://*/*任意协议且具有有效主机结构的地址,包括 abc://sss、WebSocket 地址magnet:?xt=urn:btih:...
<all_urls>任意协议的任意地址,含磁力链接等非 HTTP 地址无

注意事项 ​

WARNING

  • 协议与主机匹配不区分大小写,路径区分大小写;
  • 当前安装时只检查规则是非空字符串;无法识别的写法(如 *:* 或 host 中的 * 不在开头)不会匹配,也不会被当作全匹配;
  • 多个规则取并集:urls 数组中任一规则命中即视为匹配成功。

labels 匹配 ​

除 URL 外,还可以按请求标签匹配(常用于微应用自身创建的任务之间的协作):

json
{
  "match": {
    "urls": ["*://example.com/*"],
    "labels": ["my-miniapp"]
  }
}
  • labels 采用存在性匹配:请求标签中存在任一声明的 key 即命中;
  • 同时声明 urls 和 labels 时,任一命中即触发(两者取或)——需要「且」关系时只声明其中一组规则,把另一个条件放在脚本内自行判断;
  • 标签通常由 onStart 的 putLabel 写入,供后续 onError / onDone 匹配。

匹配与执行流程 ​

事件执行流程:事件匹配后创建独立脚本运行时、加载入口并注册回调、执行处理函数、由宿主继续处理,最后销毁运行时

多个微应用命中同一事件时,最近安装的优先,且所有匹配项都会执行。每个匹配项使用独立运行时;存在活跃 blob 数据源时,运行时保留到数据源释放。

抛出用户可见的错误 ​

脚本中 throw 普通错误只会记录日志;使用 MessageError 抛出的错误,宿主会以提示(toast)形式展示给用户:

ts
xunlei.events.onResolve(async (ctx) => {
  const parsed = await parse(ctx.req.url);
  if (!parsed) {
    throw new MessageError('该链接暂不支持解析,请检查后重试');
  }
  ctx.res = parsed;
});

MessageError仅在onResolve事件脚本中特殊处理生效,用于交付给用户错误提示。

在事件中使用 blob ​

onResolve 解析出的数据可以不经过真实 URL,直接用 blob 数据源交给下载器(需要 blob 权限;运行时会保留数据源直到下载完成):

ts
const response = await fetch(ctx.req.url);
const blob = await response.blob();

const url = await xunlei.runtime.blob.createObjectURL(blob);
ctx.res = {
  name: 'downloaded.bin',
  size: blob.size,
  files: [{ name: 'downloaded.bin', req: { url } }],
};

详见任务 API · Blob 与数据下载。

下一步 ​

  • 辅助 WebView — 在事件或页面中处理需要登录的站点
  • 示例 — 完整的事件脚本示例