Skip to content

脚本开发指南

本文档说明 JS 脚本的目录约定、manifest.json、独立任务与触发器入口,以及 ctx 常用 API。实现以当前仓库 QuickJS + betternte-script / betternte-engine 为准;若客户端行为与本文不一致,以实际版本为准或向仓库提 Issue。


目录

  1. 游戏与画面(必须)
  2. 快速开始
  3. 脚本类型
  4. 脚本清单 (manifest.json)
  5. 独立任务脚本 (solo_task)
  6. 触发器脚本 (trigger)
  7. ctx API 完整参考
  8. 最佳实践

0. 游戏与画面(必须)

游戏渲染分辨率必须为 1920×1080(1080p)。 当前脚本、模板图与 ROI 坐标均按该分辨率标定;使用其它分辨率会导致模板匹配失败、颜色检测偏移或点击位置错误。

游戏内显示设置:1920×1080(占位,请替换为真实截图)

编写脚本时请在文档与 manifestdescription 中提醒终端用户完成上述设置。用户使用说明见:游戏画面设置(必须)


1. 快速开始

目录结构

每个脚本是一个独立文件夹,放在配置的数据根下对应订阅的目录中(默认 data/local/scripts/ 为本地订阅任务脚本,data/local/triggers/ 为触发器):

data/local/scripts/
├── my_task/
│   ├── manifest.json    # 脚本清单(必需)
│   └── main.js          # 入口文件
data/local/triggers/
└── my_trigger/
    ├── manifest.json
    └── main.js

最小示例

data/local/scripts/hello_world/manifest.json

json
{
  "schema_version": 1,
  "name": "hello_world",
  "display_name": "Hello World",
  "version": "1.0.0",
  "type": "solo_task",
  "entry": "main.js"
}

data/local/scripts/hello_world/main.js

js
function init() {
  ctx.logInfo("脚本已初始化");
}

async function start() {
  ctx.logInfo("Hello from JS!");
  ctx.logInfo("配置: " + JSON.stringify(config));
}

2. 脚本类型

BetterNTE 支持三种脚本类型:

类型type入口 / 钩子用途
独立任务"solo_task"async function start();配置在全局 config用户从界面手动运行的一次性流程
触发器"trigger"每帧调用 onTrigger(ctx)(推荐);或旧式 onCapture(frame)引擎在截图 tick 中调度;ctx 与任务脚本为同一套异步桥(见第 5 节)
公共库"library"start;通过 registerLibrary 导出函数供其它脚本依赖调用,不可单独「运行」

library 类型不能被直接运行(包括 UI 运行按钮和 ctx.runScript)。推荐在消费脚本的 manifest.json 中声明 dependencies,运行时会将库的导出挂载到 ctx.<库 manifest 的 name>(例如 ctx.common_api.sum(...)),无需 ctx.call。未声明依赖时仍可使用 ctx.call("库名", "函数", args)(向后兼容)。

生命周期钩子

两种类型共享以下可选钩子:

钩子调用时机说明
init()脚本加载后初始化逻辑
onEnable()用户启用脚本时启用时的准备工作
onDisable()用户禁用脚本时清理资源
stop()任务被停止时solo_task 专用,用于中断执行
destroy()脚本卸载时最终清理

3. 脚本清单 (manifest.json)

manifest.json 是脚本的元数据声明,引擎通过它发现和加载脚本。

完整字段说明

json
{
  "schema_version": 1,
  "name": "auto_domain",
  "display_name": "自动秘境",
  "version": "1.2.0",
  "type": "solo_task",
  "entry": "main.js",
  "author": "Lorenzo",
  "description": "自动刷秘境,支持选择关卡、次数、树脂类型",
  "icon": "icon.png",
  "tags": ["combat", "domain", "farm"],
  "permissions": ["screenshot", "click"],
  "min_engine_version": "1.0.0",
  "params_schema": {
    "type": "object",
    "properties": {
      "domain_name": {
        "type": "string",
        "title": "秘境名称",
        "default": "1-7"
      },
      "repeat_count": {
        "type": "integer",
        "title": "重复次数",
        "default": 5,
        "minimum": 1,
        "maximum": 20
      }
    }
  }
}
字段类型必需说明
schema_versionnumber固定为 1
namestring唯一标识,snake_case
display_namestring界面显示名称
versionstring语义化版本号
typestring"solo_task" / "trigger" / "library"
entrystring入口 JS 文件名
authorstring作者
descriptionstring脚本描述
iconstring图标路径(相对于脚本目录)
tagsstring[]标签
permissionsstring[]声明需要的权限
min_engine_versionstring最低引擎版本
max_engine_versionstring最高引擎版本(不含)
engine_versionstring覆盖为 Cargo 风格版本要求(如 ^1.5.0
dependenciesobject[]声明 library 依赖,见下节
params_schemaobjectJSON Schema,描述配置参数(前端自动生成表单)

dependencies(库依赖)

用于 solo_task / trigger同一 JS 上下文中预加载公共库,并把导出挂到 ctx.<库 manifest 的 name> 上(与库目录里 manifest.jsonname 字段一致)。

每项结构:

字段说明
path必填。相对数据根scripts.data_root,一般为仓库 data/)的库目录路径,使用 /,如 local/scripts/common_api。不得包含 ..
min_engine_version / max_engine_version / engine_version可选。若设置,则在与库自身 manifest 合并后,用与脚本相同的规则校验当前引擎版本;未设置时仅按依赖库的 manifest 校验。

引擎会校验:路径下存在 manifest.json、类型为 library、引擎版本约束通过;然后执行库的 entry,收集 registerLibrary / exports 中的函数并挂到 ctx 上。

示例

json
"dependencies": [
  { "path": "local/scripts/common_api" }
]
js
const n = await ctx.common_api.sum({ a: 1, b: 2 });
await ctx.common_api.clickCenter({ x: 100, y: 200 });

params_schema

params_schema 使用标准 JSON Schema 格式,前端会根据它自动生成配置表单:

json
{
  "type": "object",
  "properties": {
    "repeat_count": {
      "type": "integer",
      "title": "重复次数",
      "description": "刷取次数",
      "default": 5,
      "minimum": 1,
      "maximum": 20
    },
    "use_condensed_resin": {
      "type": "boolean",
      "title": "使用浓缩树脂",
      "default": false
    },
    "region": {
      "type": "string",
      "title": "区域",
      "enum": ["mondstadt", "liyue", "fontaine"],
      "default": "fontaine"
    }
  }
}

支持的字段类型:

  • "string" → 文本输入(有 enum 时变为下拉选择)
  • "integer" / "number" → 数字输入
  • "boolean" → 开关
  • "array" → 数组

4. 独立任务脚本 (solo_task)

独立任务由用户手动触发,执行一次后结束。入口函数是 async function start()(无参数)。

引擎会把用户在界面上的配置序列化为 全局变量 config,并调用 start()不传参。若写成 async function start(config),形参 config 会是 undefined,访问 config.xxx 会直接报错。请使用全局 config 或先读取:

js
const cfg = typeof globalThis.config === "object" && globalThis.config !== null ? globalThis.config : {};

基本结构

js
// 可选:脚本加载时调用
function init() {
  ctx.logInfo("脚本已初始化");
}

// 必需:任务入口(无参数;配置在全局 config)
async function start() {
  const domain = config.domain_name || "1-7";
  // ...
}

// 可选:用户停止任务时调用
function stop() {
  // 清理逻辑
}

示例:带参数的多轮任务(伪代码)

下列坐标与步骤仅为演示;真实脚本需按 1920×1080 下标定的模板与 ROI 编写。

js
async function start() {
  const stage = config.stage_name || "default";
  const count = config.repeat_count || 5;

  ctx.logInfo(`开始: stage=${stage}, 次数=${count}`);

  for (let i = 1; i <= count; i++) {
    if (ctx.isCancelled()) break;
    ctx.progress(i - 1, count);
    ctx.logInfo(`第 ${i}/${count} 轮`);
    // await ctx.findTemplate(...) / await ctx.click(...) 等
    await ctx.sleep(500);
  }

  ctx.progress(count, count);
  return { completed: count };
}

示例:带模板匹配的战斗任务

js
async function start() {
  ctx.logInfo("开始自动战斗");

  while (!ctx.isCancelled()) {
    // findTemplate 默认复用当前 tick 的共享帧,一般无需先 capture
    const btn = await ctx.findTemplate("attack_btn.png", { threshold: 0.9 });
    if (btn) {
      // 点击按钮中心
      await ctx.click(btn.x + btn.width / 2, btn.y + btn.height / 2);
    }

    // 检查是否胜利(模板匹配)
    const victory = await ctx.findTemplate("victory.png", { threshold: 0.85 });
    if (victory) {
      ctx.logInfo("战斗胜利!");
      break;
    }

    await ctx.sleep(500);
  }

  ctx.logInfo("战斗结束");
}

示例:使用 OCR 识别

js
async function start() {
  // OCR 识别指定区域的文字
  const text = await ctx.ocr({ x: 100, y: 50, width: 300, height: 40 });
  ctx.logInfo(`识别到文字: ${text}`);

  // 等待特定文字出现
  // (循环检测方式)
  for (let i = 0; i < 30; i++) {
    const t = await ctx.ocr({ x: 400, y: 300, width: 200, height: 50 });
    if (t.includes("完成")) {
      ctx.logInfo("检测到完成");
      break;
    }
    await ctx.sleep(1000);
  }
}

5. 触发器脚本 (trigger)

触发器在引擎 每次截图 tick 中调度:对已启用的触发器脚本调用 QuickJS 钩子。实现上优先调用 onTrigger(ctx);若未定义,则回退到旧式 onCapture(frame)

5.1 与独立任务使用同一套 ctx

onTrigger 的参数 ctx 与全局 ctx 为同一对象rquickjs 里从 globalThis.ctx 传入),API 与 §6 中列出的异步方法一致,例如 await ctx.findTemplate(...)await ctx.click(...)。引擎在每帧调用后会 排空 QuickJS 微任务队列,因此 async function onTrigger(ctx) 同样可用(仍建议单帧内少做重活,避免拖慢整路截图)。

用户在界面为触发器保存的配置会注入为:

  • ctx.params(以及兼容全局 $params

不推荐 依赖旧式 onCapture(frame):当前回退路径里传入的 frame 可能 仅含宽高元数据,不含像素;若需画面请使用 await ctx.capture() 或依赖识别 API 自带的 共享帧 / 缓存帧(与独立任务相同)。

5.2 推荐写法(每帧轻量判断)

js
async function onTrigger(ctx) {
  const p = ctx.params || {};
  const threshold = p.threshold ?? 0.85;
  const hit = await ctx.findTemplate("skip_hint.png", { threshold });
  if (!hit) return;
  const cx = hit.x + hit.width / 2;
  const cy = hit.y + hit.height / 2;
  await ctx.click(cx, cy);
}

5.3 生命周期(与运行时一致)

  • 启用触发器时:引擎调用 onEnable(若存在),并写入 ctx.params
  • 每帧:onTriggeronCapture
  • 禁用:onDisable(若存在)。

5.4 触发器 vs 独立任务

维度独立任务 (solo_task)触发器 (trigger)
入口async function start(),使用全局 configonTrigger(ctx)(推荐)或 onCapture(frame)
调度用户点击运行引擎按帧自动调用
ctx全局 ctx同一 ctx 对象
进度 / 取消ctx.progressctx.isCancelled()一般不用;长耗时逻辑建议放到任务脚本
典型用途多步、可中断的流程轻量检测 + 少量点击

6. ctx API 完整参考

下列方法挂载在 globalThis.ctx 上;独立任务触发器onTrigger)使用同一套 API(见 §5)。方法均为 async,从 JS 侧 await;底层通过 __invoke 同步桥接回 Rust。

6.1 状态与控制

ctx.isCancelled() → bool

检查任务是否已被用户取消。在长循环中应频繁调用。

js
for (let i = 0; i < 100; i++) {
  if (ctx.isCancelled()) break;
  // ...
}

ctx.progress(current, total)

上报进度,前端会显示进度条。

js
ctx.progress(3, 10); // 3/10

6.2 截图与识别

await ctx.capture(force?) → { width, height, data_len }

获取当前帧(默认尽量复用引擎 共享帧force === true 时强制重新截图)。返回值 不包含 原始像素缓冲(体积过大),仅元数据。

js
const frame = await ctx.capture();
// frame: { width: 1920, height: 1080, data_len: ... }  // 分辨率须与游戏设置一致,见 §0

await ctx.captureRegion(x, y, w, h, force?) → { width, height }

截取指定区域;返回值为 元数据(与 capture 一致,不含像素缓冲)。

js
const meta = await ctx.captureRegion(100, 200, 400, 300);

await ctx.findTemplate(name, opts?) → MatchResult | null

模板匹配,在当前绑定窗口的截图中查找与模板图一致的区域。

磁盘路径(重要):引擎以当前脚本的 manifest 所在目录 为根,在子目录 templates/ 下加载模板图。name"foo""foo.png" 均解析为文件 …/<脚本目录>/templates/foo.png(无扩展名时默认补 .png)。请勿把模板 PNG 仅放在脚本根目录(与 main.js 同级),否则无法找到。

js
// 基本用法
const match = await ctx.findTemplate("btn.png");
if (match) {
  ctx.logInfo(`找到: (${match.x}, ${match.y}) 置信度=${match.confidence}`);
}

// 带选项
const match = await ctx.findTemplate("btn.png", {
  threshold: 0.9,    // 匹配阈值 (0-1)
  roi: { x: 0, y: 0, width: 960, height: 540 }  // 限定搜索区域
});

MAA 对齐的模板遮罩(不规则图标 / 透明底):

字段类型默认说明
greenMaskbooleanfalsetrue 时,模板里 纯绿 #00FF00(在 greenMaskTolerance 内)的像素 不参与 NCC,与 MAA 绿幕素材一致
greenMaskTolerancenumber0绿幕判定:各通道与 R=0、G=255、B=0 的最大允许偏差
useAlphaMaskbooleanfalsetrue 时,模板 alpha alphaMaskThreshold 的像素不参与匹配(适合带透明通道的 PNG)
alphaMaskThresholdnumber8视为「全透明」的上限(含边界);opaque 为 255

可同时开 greenMaskuseAlphaMask:某一像素在 任一条规则 下被遮罩则权重为 0。

js
const icon = await ctx.findTemplate("bag_with_dot.png", {
  threshold: 0.85,
  greenMask: true,
  greenMaskTolerance: 0,
});
const pngIcon = await ctx.findTemplate("ui_alpha.png", {
  threshold: 0.8,
  useAlphaMask: true,
  alphaMaskThreshold: 16,
});

返回值 MatchResult:

js
{
  x: 100,        // 左上角 x
  y: 200,        // 左上角 y
  width: 80,     // 匹配宽度
  height: 30,    // 匹配高度
  confidence: 0.95  // 置信度
}

await ctx.findTemplateBatch(entries) → (MatchResult | null)[]

在同一 缓存帧 上依次执行多次模板匹配:只解码一次 整帧为 DynamicImage,每个条目再按自己的 roi 裁剪并跑 NCC。返回数组长度与 entries 一致,未命中为 null

entries 为对象数组,每项为 { name, ...与 findTemplate 第二参数相同的字段 }name 也可用别名 template)。与 findTemplate 共用 greenMaskuseAlphaMaskthresholdroi 等选项。

当前 QuickJS 桥里 ctx.*Promise 在构造时 同步 调用 Rust,因此 Promise.all([ctx.findTemplate(...), ...]) 并不会并行,只是语法上的「同时发起」;要减少重复取帧与解码,请用 findTemplateBatch

js
const roi = { x: 577, y: 55, width: 784, height: 100 };
const [left, right, player] = await ctx.findTemplateBatch([
  { name: "耐力条左侧", roi, useAlphaMask: true, threshold: 0.9 },
  { name: "耐力条右侧", roi, useAlphaMask: true, threshold: 0.9 },
  { name: "玩家控制区", roi, useAlphaMask: true, threshold: 0.9 },
]);

await ctx.ocr(region) → string

OCR 文字识别。

js
const text = await ctx.ocr({ x: 100, y: 50, width: 300, height: 40 });
ctx.logInfo(`识别结果: ${text}`);

await ctx.ocrAll() → OcrResult[]

当前缓存/共享帧 做整帧 OCR,返回所有识别区域(无参数;不要传入 frame)。

js
const results = await ctx.ocrAll();
for (const r of results) {
  ctx.logInfo(`"${r.text}" bbox conf=${r.confidence}`);
}

await ctx.getColor(x, y) → string

获取指定坐标的颜色值(十六进制)。

js
const color = await ctx.getColor(960, 540);
ctx.logInfo(`颜色: ${color}`); // "#ff8800"

await ctx.colorMatch(x, y, color, tolerance) → bool

判断指定坐标的颜色是否匹配。

js
const isRed = await ctx.colorMatch(100, 200, "#ff0000", 30);
if (isRed) {
  ctx.logInfo("检测到红色");
}

await ctx.scanSliderStrip(opts) → object

单次取帧,在 opts.region 矩形内沿一行(默认 rowOffset 为区域高度的一半)按 stepX 步进采样,用颜色容差找 耐力条色玩家色 各自最宽的水平连通段,返回条左右与中心、玩家中心。需要 manifest 权限 color_detect

opts(camelCase):region { x, y, width, height }barColorplayerColor;可选 barTolerance / playerTolerance(欧氏 u8,默认 28 / 24),stepX(默认 2),rowOffsetminBarRunPx / minPlayerRunPx(像素,默认 18 / 6)。

成功时:{ ok: true, bar_left, bar_right, bar_center, player_left, player_right, player_center, row_screen_y, step_x };失败时:ok: falsereason(如 no_bar / no_player / no_color_detector)。

js
const r = await ctx.scanSliderStrip({
  region: { x: 581, y: 53, width: 767, height: 31 },
  barColor: "#25BCA6",
  playerColor: "#FEF7A4",
  barTolerance: 26,
  playerTolerance: 24,
  stepX: 2,
});
if (r.ok) ctx.logInfo(`条心=${r.bar_center} 玩家=${r.player_center}`);

await ctx.colorMatchAll(points[, options])

多点颜色匹配:仅当所有点都匹配时整体为真。

推荐:第二个参数为 options 对象(camelCase 键名,与下表一致):

字段类型默认说明
defaultTolerancenumber32欧氏容差:未写 tolerance / rgbaTolerance 的点,在 RGB 空间用「与目标色的欧氏距离 ≤ 该值」判定(与单参数 colorMatch 一致;目标 alpha 不参与距离)
defaultRgbaTolerance{ r, g, b, a }可选。未写 tolerance 且未写 rgbaTolerance 的点,改用「各通道与目标色之差的绝对值 ≤ 对应上限」判定。截图像素按 alpha 255 与目标的 A 比较
debugbooleanfalsetrue 时返回 JSON 详情对象;为 false 时返回 boolean
shiftMax{ maxDx, maxDy }可选。对同一整数偏移同时平移所有点做联合匹配;每轴限制 128。先试 (0,0)

每个点可写 tolerance(数字,欧氏)或 rgbaTolerance(对象,分通道)。若写了 rgbaTolerance,该点不再使用 tolerance / 默认欧氏值。优先级:rgbaTolerancetolerancedefaultRgbaTolerancedefaultTolerance

debug: true 时,每条点结果含 tolerance(欧氏模式下为所用阈值;仅用 rgbaTolerance 时为 0)与可选的 rgbaTolerance(分通道模式时回显所用上限)。

  • debug === false:返回 boolean
  • debug === true:返回 JSON(顶层多为 snake_case,部分嵌套字段为 camelCase),含 all_matchmatched_shift(成功时为 { x, y })、points(每项含 x, y, sample_x, sample_y, expected, actual, tolerance, 可选 rgbaTolerance, matched)。

兼容旧写法(仍支持):colorMatchAll(points, defaultTolerance)colorMatchAll(points, tol, debug)colorMatchAll(points, tol, debug, shiftMax对象) — 与原先多参数顺序一致。

js
const ok = await ctx.colorMatchAll(
  [
    { x: 100, y: 200, color: "#ff0000", tolerance: 20 },
    { x: 160, y: 240, color: "#00ff00" },
    { x: 220, y: 260, color: "#0000ff", tolerance: 25 },
  ],
  { defaultTolerance: 30, debug: false }
);

const detail = await ctx.colorMatchAll(
  [{ x: 100, y: 200, color: "#ffffff" }],
  { defaultTolerance: 32, debug: true }
);
ctx.logInfo(JSON.stringify(detail));

// 分通道容差示例:R/G/B 各允许偏差 12,A 允许 255(等价于不限制 alpha)
const rgbaOk = await ctx.colorMatchAll(
  [{ x: 10, y: 10, color: "#FFEEDD", rgbaTolerance: { r: 12, g: 12, b: 12, a: 255 } }],
  { debug: false }
);

const withShift = await ctx.colorMatchAll(points, {
  defaultTolerance: 32,
  debug: true,
  shiftMax: { maxDx: 40, maxDy: 30 },
});
if (withShift.all_match) {
  ctx.logInfo(`偏移: ${withShift.matched_shift.x}, ${withShift.matched_shift.y}`);
}

6.3 输入操作

await ctx.click(x, y)

鼠标左键点击。

js
await ctx.click(960, 540);

await ctx.doubleClick(x, y)

鼠标双击。

js
await ctx.doubleClick(960, 540);

await ctx.rightClick(x, y)

鼠标右键点击。

js
await ctx.rightClick(960, 540);

await ctx.mouseMove(x, y)

移动鼠标(不点击)。

js
await ctx.mouseMove(500, 300);

await ctx.mouseDown(button) / await ctx.mouseUp(button)

按下/释放鼠标按钮,可用于拖拽或组合操作。

button 支持:"left" / "right" / "middle" / "x1" / "x2"

js
await ctx.mouseDown("middle");
await ctx.mouseMove(1200, 550);
await ctx.mouseUp("middle");

await ctx.scroll(delta)

鼠标滚轮。正数向上,负数向下。

js
await ctx.scroll(-3);   // 向下滚 3 格
await ctx.scroll(5);    // 向上滚 5 格

await ctx.swipe(x1, y1, x2, y2, duration_ms)

模拟滑动手势。

js
// 从 (500, 800) 滑到 (500, 200),耗时 300ms
await ctx.swipe(500, 800, 500, 200, 300);

await ctx.keyPress(key, duration_ms?)

按下并释放一个键。

js
await ctx.keyPress("F");
await ctx.keyPress("Space");
await ctx.keyPress("Enter");

await ctx.keyDown(key) / await ctx.keyUp(key)

单独按下/释放键(用于组合键)。

js
await ctx.keyDown("Shift");
await ctx.keyPress("Tab");
await ctx.keyUp("Shift");

await ctx.keyCombo(keys)

按顺序按下并释放多个按键(组合键)。

js
await ctx.keyCombo(["Ctrl", "C"]);
await ctx.keyCombo(["Alt", "Tab"]);

await ctx.typeText(text)

输入文本。

js
await ctx.typeText("Hello World");

6.4 等待操作

await ctx.sleep(ms)

等待指定毫秒。

js
await ctx.sleep(2000); // 等 2 秒

await ctx.waitForTemplate(name, timeout_ms, opts?) → MatchResult | null

等待模板图片出现。每 200ms 轮询一次。可选第三参数 optsfindTemplate(name, opts) 相同(roithreshold);不传则全屏匹配。

js
// 等待"确认"按钮出现,最多等 10 秒
const btn = await ctx.waitForTemplate("confirm_btn.png", 10000);
if (btn) {
  await ctx.click(btn.x + btn.width / 2, btn.y + btn.height / 2);
} else {
  ctx.logWarn("超时:未找到确认按钮");
}

// 仅在 ROI 内等待(与 findTemplate 一致)
const btn2 = await ctx.waitForTemplate("confirm_btn.png", 10000, {
  roi: { x: 400, y: 300, width: 500, height: 200 },
  threshold: 0.85,
});

await ctx.waitGone(name, timeout_ms) → bool

等待模板图片消失。

js
// 等待加载画面消失
const gone = await ctx.waitGone("loading.png", 30000);
if (gone) {
  ctx.logInfo("加载完成");
}

await ctx.waitForColor(x, y, color, timeout_ms) → bool

等待指定坐标变为指定颜色。

js
// 等待 (960, 540) 变为绿色
const matched = await ctx.waitForColor(960, 540, "#00ff00", 5000);

6.5 窗口操作

await ctx.findWindow(title) → number | null

按标题关键词查找窗口,返回窗口句柄。

js
const hwnd = await ctx.findWindow("MyGame");
if (hwnd) {
  ctx.logInfo(`找到窗口: ${hwnd}`);
}

await ctx.activateWindow(hwnd)

激活窗口(置前台)。

js
await ctx.activateWindow(hwnd);

await ctx.getWindowRect(hwnd) → Rect

获取窗口位置和大小。

js
const rect = await ctx.getWindowRect(hwnd);
// rect: { x, y, width, height }

await ctx.getScreenSize() → [number, number]

获取屏幕分辨率。

js
const [w, h] = await ctx.getScreenSize();
ctx.logInfo(`屏幕: ${w}x${h}`);

6.6 脚本间调用

await ctx.runScript(name, params) → any

调用其他脚本。

仅用于运行 solo_task 脚本;library 类型会报错。

js
const result = await ctx.runScript("auto_pick", { range: 3.0 });
ctx.logInfo(`拾取结果: ${JSON.stringify(result)}`);

await ctx.call(library, fnName, args) → any

调用公共库脚本(type: "library")中导出的函数。库函数内可照常 await ctx.click / ctx.capture 等,与调用方共用同一 ScriptContext(权限、窗口与存储作用域以引擎实现为准)。

导出查找顺序:

  • globalThis.__libraryExports[fnName]
  • exports[fnName]
  • globalThis[fnName]

推荐写法(library 脚本):引擎会注入 registerLibrary(与 ctx.registerLibrary 相同),一次性写入 exports__libraryExports

js
// data/local/scripts/common_api/main.js (type: "library")
registerLibrary("add", async function (args) {
  const a = Number(args?.a ?? 0);
  const b = Number(args?.b ?? 0);
  return a + b;
});

调用示例(task/trigger 脚本):

js
const sum = await ctx.call("common_api", "add", { a: 1, b: 2 });
ctx.logInfo(`sum=${sum}`);

快速复制片段(与脚本调试页一致):

js
// 调用片段(task/trigger)
const sum = await ctx.call("common_api", "sum", { a: 1, b: 2 });
ctx.logInfo("sum=" + sum);
js
// 导出片段(library)
registerLibrary("sum", async function (args) {
  return Number(args?.a ?? 0) + Number(args?.b ?? 0);
});

6.7 日志与通知

ctx.log(level, message)

输出指定级别的日志。

js
ctx.log("info", "信息");
ctx.log("warn", "警告");
ctx.log("error", "错误");
ctx.log("debug", "调试");

ctx.logInfo(msg) / ctx.logWarn(msg) / ctx.logError(msg) / ctx.logDebug(msg)

快捷日志方法。

js
ctx.logInfo("操作完成");
ctx.logWarn("资源不足");
ctx.logError("执行失败");
ctx.logDebug("调试信息");

ctx.notify(title, body)

发送系统通知。

js
ctx.notify("任务完成", "自动秘境已刷完 10 次");

6.8 文件操作

await ctx.readFile(path) → string

读取文件内容。

js
const content = await ctx.readFile("config/my_settings.json");
const data = JSON.parse(content);

await ctx.writeFile(path, content)

写入文件。

js
await ctx.writeFile("output/result.json", JSON.stringify({ success: true }));

6.9 网络请求

await ctx.httpGet(url) → string

HTTP GET 请求。

js
const response = await ctx.httpGet("https://api.example.com/status");

await ctx.httpPost(url, body) → string

HTTP POST 请求。

js
const response = await ctx.httpPost(
  "https://api.example.com/report",
  JSON.stringify({ task: "auto_domain", count: 10 })
);

6.10 本地存储

await ctx.storageGet(key) → any | null

读取持久化存储。

js
const lastRun = await ctx.storageGet("last_run_time");

await ctx.storageSet(key, value)

写入持久化存储。

js
await ctx.storageSet("last_run_time", Date.now());
await ctx.storageSet("run_count", 42);

await ctx.storageDelete(key)

删除存储项。

js
await ctx.storageDelete("temp_data");

7. 最佳实践

7.1 检查取消状态

在长时间循环中频繁检查 ctx.isCancelled()

js
for (let i = 0; i < count; i++) {
  if (ctx.isCancelled()) break;  // 用户可以随时停止
  // ... 执行逻辑
}

7.2 报告进度

让前端显示进度条:

js
for (let i = 0; i < total; i++) {
  ctx.progress(i, total);
  // ...
}
ctx.progress(total, total);

7.3 使用 params_schema

声明参数 schema,让前端自动生成配置表单,而不是硬编码:

js
// manifest.json 中声明 params_schema
// main.js 中通过 config 读取
const count = config.repeat_count || 5;

7.4 错误处理

加载期错误main.js 语法错误、缺 start/main 等)与未捕获的运行时错误会由引擎写入 Error 级别日志(与 ctx.logError 一样会出现在客户端日志/任务记录中),并在终端 tracing 中输出,便于与「只在浏览器控制台看 Log」区分。

js
async function start() {
  try {
    const frame = await ctx.capture();
    if (!frame) {
      ctx.logError("截图失败");
      return;
    }
    // ...
  } catch (e) {
    ctx.logError("执行出错: " + e);
  }
}

7.5 触发器状态管理

ctx没有 引擎注入的 ctx.state 字段。跨帧持久状态请使用 模块级变量 或挂到 globalThis(注意命名避免冲突):

js
let __lastAction = 0;

async function onTrigger(ctx) {
  const now = Date.now();
  if (now - __lastAction < 500) return; // 500ms 冷却
  __lastAction = now;
  // ...
}

7.6 模板图片存放

模板图片须放在脚本目录下的 templates/ 子目录中(与 manifest.json 同级目录下的 templates 文件夹):

data/local/scripts/auto_claim/
├── manifest.json
├── main.js
└── templates/
    ├── claim_btn.png
    ├── confirm_btn.png
    └── receive_btn.png

ctx.findTemplate 只传 文件名(可带或不带 .png),引擎会到 templates/ 内解析:

js
const match = await ctx.findTemplate("claim_btn.png");
// 等价:await ctx.findTemplate("claim_btn");

7.7 返回值

脚本的 start() 函数可以返回一个对象,作为执行结果:

js
async function start() {
  // ...
  return {
    completed: 10,
    duration_ms: 120000,
    success: true
  };
}

7.8 性能行为与调优建议(无新 API)

ctx 当前在引擎内部默认启用同帧复用优化,不需要改脚本调用方式:

  • shared frame 优先:识别相关调用优先复用同一 tick 的共享帧,其次使用帧缓存,最后才回退到实时截图。
  • 同帧只解码一次findTemplate*ocr*color* 在同一帧内共享解码后的图像,减少重复 to_dynamic_image
  • 模板 decode 缓存:模板按「templates/ 下规范化绝对路径 + 文件修改时间」缓存;切换脚本(set_template_dir)会清空缓存,文件改动会自动失效重载。
  • OCR 批处理复用(保守)ctx.ocr(...) 对“大区域”会优先走一次整帧 OCR,并把结果复用给同帧后续 OCR 查询;不满足条件时自动降级为单区域识别。

更容易触发复用的写法:

js
// 推荐:同一轮逻辑内连续做识别,不要在每次识别间 sleep / 强制 capture
const [btnA, btnB] = await ctx.findTemplateBatch([
  { name: "按钮A", threshold: 0.9 },
  { name: "按钮B", threshold: 0.9 },
]);
const title = await ctx.ocr({ x: 300, y: 120, width: 900, height: 280 });
const ok = await ctx.colorMatch(640, 420, "#00ff88", 24);
js
// 不推荐:在同一判断轮里多次 force capture,会打断复用路径
await ctx.capture(true);
const a = await ctx.findTemplate("按钮A");
await ctx.capture(true);
const b = await ctx.findTemplate("按钮B");

性能观测(默认低开销):

  • 设置环境变量 BETTERNTE_PERF_LOG=steps:输出慢调用和关键命中事件。
  • 设置 BETTERNTE_PERF_LOG=verbose:输出更完整的性能事件(如 frame decode/命中明细)。
  • 可选阈值 BETTERNTE_PERF_CTX_SLOW_MS:调整 ctx 慢调用阈值(毫秒)。

Released under the MIT License.