脚本开发指南
本文档说明 JS 脚本的目录约定、
manifest.json、独立任务与触发器入口,以及ctx常用 API。实现以当前仓库 QuickJS +betternte-script/betternte-engine为准;若客户端行为与本文不一致,以实际版本为准或向仓库提 Issue。
目录
0. 游戏与画面(必须)
游戏渲染分辨率必须为 1920×1080(1080p)。 当前脚本、模板图与 ROI 坐标均按该分辨率标定;使用其它分辨率会导致模板匹配失败、颜色检测偏移或点击位置错误。
编写脚本时请在文档与 manifest 的 description 中提醒终端用户完成上述设置。用户使用说明见:游戏画面设置(必须)。
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
{
"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
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 是脚本的元数据声明,引擎通过它发现和加载脚本。
完整字段说明
{
"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_version | number | 是 | 固定为 1 |
name | string | 是 | 唯一标识,snake_case |
display_name | string | 是 | 界面显示名称 |
version | string | 是 | 语义化版本号 |
type | string | 是 | "solo_task" / "trigger" / "library" |
entry | string | 是 | 入口 JS 文件名 |
author | string | 否 | 作者 |
description | string | 否 | 脚本描述 |
icon | string | 否 | 图标路径(相对于脚本目录) |
tags | string[] | 否 | 标签 |
permissions | string[] | 否 | 声明需要的权限 |
min_engine_version | string | 否 | 最低引擎版本 |
max_engine_version | string | 否 | 最高引擎版本(不含) |
engine_version | string | 否 | 覆盖为 Cargo 风格版本要求(如 ^1.5.0) |
dependencies | object[] | 否 | 声明 library 依赖,见下节 |
params_schema | object | 否 | JSON Schema,描述配置参数(前端自动生成表单) |
dependencies(库依赖)
用于 solo_task / trigger 在同一 JS 上下文中预加载公共库,并把导出挂到 ctx.<库 manifest 的 name> 上(与库目录里 manifest.json 的 name 字段一致)。
每项结构:
| 字段 | 说明 |
|---|---|
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 上。
示例
"dependencies": [
{ "path": "local/scripts/common_api" }
]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 格式,前端会根据它自动生成配置表单:
{
"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 或先读取:
const cfg = typeof globalThis.config === "object" && globalThis.config !== null ? globalThis.config : {};基本结构
// 可选:脚本加载时调用
function init() {
ctx.logInfo("脚本已初始化");
}
// 必需:任务入口(无参数;配置在全局 config)
async function start() {
const domain = config.domain_name || "1-7";
// ...
}
// 可选:用户停止任务时调用
function stop() {
// 清理逻辑
}示例:带参数的多轮任务(伪代码)
下列坐标与步骤仅为演示;真实脚本需按 1920×1080 下标定的模板与 ROI 编写。
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 };
}示例:带模板匹配的战斗任务
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 识别
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 推荐写法(每帧轻量判断)
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。 - 每帧:
onTrigger或onCapture。 - 禁用:
onDisable(若存在)。
5.4 触发器 vs 独立任务
| 维度 | 独立任务 (solo_task) | 触发器 (trigger) |
|---|---|---|
| 入口 | async function start(),使用全局 config | onTrigger(ctx)(推荐)或 onCapture(frame) |
| 调度 | 用户点击运行 | 引擎按帧自动调用 |
ctx | 全局 ctx | 同一 ctx 对象 |
| 进度 / 取消 | ctx.progress、ctx.isCancelled() | 一般不用;长耗时逻辑建议放到任务脚本 |
| 典型用途 | 多步、可中断的流程 | 轻量检测 + 少量点击 |
6. ctx API 完整参考
下列方法挂载在
globalThis.ctx上;独立任务与 触发器(onTrigger)使用同一套 API(见 §5)。方法均为 async,从 JS 侧await;底层通过__invoke同步桥接回 Rust。
6.1 状态与控制
ctx.isCancelled() → bool
检查任务是否已被用户取消。在长循环中应频繁调用。
for (let i = 0; i < 100; i++) {
if (ctx.isCancelled()) break;
// ...
}ctx.progress(current, total)
上报进度,前端会显示进度条。
ctx.progress(3, 10); // 3/106.2 截图与识别
await ctx.capture(force?) → { width, height, data_len }
获取当前帧(默认尽量复用引擎 共享帧;force === true 时强制重新截图)。返回值 不包含 原始像素缓冲(体积过大),仅元数据。
const frame = await ctx.capture();
// frame: { width: 1920, height: 1080, data_len: ... } // 分辨率须与游戏设置一致,见 §0await ctx.captureRegion(x, y, w, h, force?) → { width, height }
截取指定区域;返回值为 元数据(与 capture 一致,不含像素缓冲)。
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 同级),否则无法找到。
// 基本用法
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 对齐的模板遮罩(不规则图标 / 透明底):
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
greenMask | boolean | false | 为 true 时,模板里 纯绿 #00FF00(在 greenMaskTolerance 内)的像素 不参与 NCC,与 MAA 绿幕素材一致 |
greenMaskTolerance | number | 0 | 绿幕判定:各通道与 R=0、G=255、B=0 的最大允许偏差 |
useAlphaMask | boolean | false | 为 true 时,模板 alpha ≤ alphaMaskThreshold 的像素不参与匹配(适合带透明通道的 PNG) |
alphaMaskThreshold | number | 8 | 视为「全透明」的上限(含边界);opaque 为 255 |
可同时开 greenMask 与 useAlphaMask:某一像素在 任一条规则 下被遮罩则权重为 0。
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:
{
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 共用 greenMask、useAlphaMask、threshold、roi 等选项。
当前 QuickJS 桥里 ctx.* 的 Promise 在构造时 同步 调用 Rust,因此 Promise.all([ctx.findTemplate(...), ...]) 并不会并行,只是语法上的「同时发起」;要减少重复取帧与解码,请用 findTemplateBatch。
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 文字识别。
const text = await ctx.ocr({ x: 100, y: 50, width: 300, height: 40 });
ctx.logInfo(`识别结果: ${text}`);await ctx.ocrAll() → OcrResult[]
对 当前缓存/共享帧 做整帧 OCR,返回所有识别区域(无参数;不要传入 frame)。
const results = await ctx.ocrAll();
for (const r of results) {
ctx.logInfo(`"${r.text}" bbox conf=${r.confidence}`);
}await ctx.getColor(x, y) → string
获取指定坐标的颜色值(十六进制)。
const color = await ctx.getColor(960, 540);
ctx.logInfo(`颜色: ${color}`); // "#ff8800"await ctx.colorMatch(x, y, color, tolerance) → bool
判断指定坐标的颜色是否匹配。
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 },barColor,playerColor;可选 barTolerance / playerTolerance(欧氏 u8,默认 28 / 24),stepX(默认 2),rowOffset,minBarRunPx / minPlayerRunPx(像素,默认 18 / 6)。
成功时:{ ok: true, bar_left, bar_right, bar_center, player_left, player_right, player_center, row_screen_y, step_x };失败时:ok: false 与 reason(如 no_bar / no_player / no_color_detector)。
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 键名,与下表一致):
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
defaultTolerance | number | 32 | 欧氏容差:未写 tolerance / rgbaTolerance 的点,在 RGB 空间用「与目标色的欧氏距离 ≤ 该值」判定(与单参数 colorMatch 一致;目标 alpha 不参与距离) |
defaultRgbaTolerance | { r, g, b, a } | 无 | 可选。未写 tolerance 且未写 rgbaTolerance 的点,改用「各通道与目标色之差的绝对值 ≤ 对应上限」判定。截图像素按 alpha 255 与目标的 A 比较 |
debug | boolean | false | 为 true 时返回 JSON 详情对象;为 false 时返回 boolean |
shiftMax | { maxDx, maxDy } | 无 | 可选。对同一整数偏移同时平移所有点做联合匹配;每轴限制 128。先试 (0,0) |
每个点可写 tolerance(数字,欧氏)或 rgbaTolerance(对象,分通道)。若写了 rgbaTolerance,该点不再使用 tolerance / 默认欧氏值。优先级:rgbaTolerance > tolerance > defaultRgbaTolerance > defaultTolerance。
debug: true 时,每条点结果含 tolerance(欧氏模式下为所用阈值;仅用 rgbaTolerance 时为 0)与可选的 rgbaTolerance(分通道模式时回显所用上限)。
debug === false:返回boolean。debug === true:返回 JSON(顶层多为 snake_case,部分嵌套字段为 camelCase),含all_match、matched_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对象) — 与原先多参数顺序一致。
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)
鼠标左键点击。
await ctx.click(960, 540);await ctx.doubleClick(x, y)
鼠标双击。
await ctx.doubleClick(960, 540);await ctx.rightClick(x, y)
鼠标右键点击。
await ctx.rightClick(960, 540);await ctx.mouseMove(x, y)
移动鼠标(不点击)。
await ctx.mouseMove(500, 300);await ctx.mouseDown(button) / await ctx.mouseUp(button)
按下/释放鼠标按钮,可用于拖拽或组合操作。
button 支持:"left" / "right" / "middle" / "x1" / "x2"。
await ctx.mouseDown("middle");
await ctx.mouseMove(1200, 550);
await ctx.mouseUp("middle");await ctx.scroll(delta)
鼠标滚轮。正数向上,负数向下。
await ctx.scroll(-3); // 向下滚 3 格
await ctx.scroll(5); // 向上滚 5 格await ctx.swipe(x1, y1, x2, y2, duration_ms)
模拟滑动手势。
// 从 (500, 800) 滑到 (500, 200),耗时 300ms
await ctx.swipe(500, 800, 500, 200, 300);await ctx.keyPress(key, duration_ms?)
按下并释放一个键。
await ctx.keyPress("F");
await ctx.keyPress("Space");
await ctx.keyPress("Enter");await ctx.keyDown(key) / await ctx.keyUp(key)
单独按下/释放键(用于组合键)。
await ctx.keyDown("Shift");
await ctx.keyPress("Tab");
await ctx.keyUp("Shift");await ctx.keyCombo(keys)
按顺序按下并释放多个按键(组合键)。
await ctx.keyCombo(["Ctrl", "C"]);
await ctx.keyCombo(["Alt", "Tab"]);await ctx.typeText(text)
输入文本。
await ctx.typeText("Hello World");6.4 等待操作
await ctx.sleep(ms)
等待指定毫秒。
await ctx.sleep(2000); // 等 2 秒await ctx.waitForTemplate(name, timeout_ms, opts?) → MatchResult | null
等待模板图片出现。每 200ms 轮询一次。可选第三参数 opts 与 findTemplate(name, opts) 相同(roi、threshold);不传则全屏匹配。
// 等待"确认"按钮出现,最多等 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
等待模板图片消失。
// 等待加载画面消失
const gone = await ctx.waitGone("loading.png", 30000);
if (gone) {
ctx.logInfo("加载完成");
}await ctx.waitForColor(x, y, color, timeout_ms) → bool
等待指定坐标变为指定颜色。
// 等待 (960, 540) 变为绿色
const matched = await ctx.waitForColor(960, 540, "#00ff00", 5000);6.5 窗口操作
await ctx.findWindow(title) → number | null
按标题关键词查找窗口,返回窗口句柄。
const hwnd = await ctx.findWindow("MyGame");
if (hwnd) {
ctx.logInfo(`找到窗口: ${hwnd}`);
}await ctx.activateWindow(hwnd)
激活窗口(置前台)。
await ctx.activateWindow(hwnd);await ctx.getWindowRect(hwnd) → Rect
获取窗口位置和大小。
const rect = await ctx.getWindowRect(hwnd);
// rect: { x, y, width, height }await ctx.getScreenSize() → [number, number]
获取屏幕分辨率。
const [w, h] = await ctx.getScreenSize();
ctx.logInfo(`屏幕: ${w}x${h}`);6.6 脚本间调用
await ctx.runScript(name, params) → any
调用其他脚本。
仅用于运行
solo_task脚本;library类型会报错。
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。
// 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 脚本):
const sum = await ctx.call("common_api", "add", { a: 1, b: 2 });
ctx.logInfo(`sum=${sum}`);快速复制片段(与脚本调试页一致):
// 调用片段(task/trigger)
const sum = await ctx.call("common_api", "sum", { a: 1, b: 2 });
ctx.logInfo("sum=" + sum);// 导出片段(library)
registerLibrary("sum", async function (args) {
return Number(args?.a ?? 0) + Number(args?.b ?? 0);
});6.7 日志与通知
ctx.log(level, message)
输出指定级别的日志。
ctx.log("info", "信息");
ctx.log("warn", "警告");
ctx.log("error", "错误");
ctx.log("debug", "调试");ctx.logInfo(msg) / ctx.logWarn(msg) / ctx.logError(msg) / ctx.logDebug(msg)
快捷日志方法。
ctx.logInfo("操作完成");
ctx.logWarn("资源不足");
ctx.logError("执行失败");
ctx.logDebug("调试信息");ctx.notify(title, body)
发送系统通知。
ctx.notify("任务完成", "自动秘境已刷完 10 次");6.8 文件操作
await ctx.readFile(path) → string
读取文件内容。
const content = await ctx.readFile("config/my_settings.json");
const data = JSON.parse(content);await ctx.writeFile(path, content)
写入文件。
await ctx.writeFile("output/result.json", JSON.stringify({ success: true }));6.9 网络请求
await ctx.httpGet(url) → string
HTTP GET 请求。
const response = await ctx.httpGet("https://api.example.com/status");await ctx.httpPost(url, body) → string
HTTP POST 请求。
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
读取持久化存储。
const lastRun = await ctx.storageGet("last_run_time");await ctx.storageSet(key, value)
写入持久化存储。
await ctx.storageSet("last_run_time", Date.now());
await ctx.storageSet("run_count", 42);await ctx.storageDelete(key)
删除存储项。
await ctx.storageDelete("temp_data");7. 最佳实践
7.1 检查取消状态
在长时间循环中频繁检查 ctx.isCancelled():
for (let i = 0; i < count; i++) {
if (ctx.isCancelled()) break; // 用户可以随时停止
// ... 执行逻辑
}7.2 报告进度
让前端显示进度条:
for (let i = 0; i < total; i++) {
ctx.progress(i, total);
// ...
}
ctx.progress(total, total);7.3 使用 params_schema
声明参数 schema,让前端自动生成配置表单,而不是硬编码:
// manifest.json 中声明 params_schema
// main.js 中通过 config 读取
const count = config.repeat_count || 5;7.4 错误处理
加载期错误(main.js 语法错误、缺 start/main 等)与未捕获的运行时错误会由引擎写入 Error 级别日志(与 ctx.logError 一样会出现在客户端日志/任务记录中),并在终端 tracing 中输出,便于与「只在浏览器控制台看 Log」区分。
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(注意命名避免冲突):
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.pngctx.findTemplate 只传 文件名(可带或不带 .png),引擎会到 templates/ 内解析:
const match = await ctx.findTemplate("claim_btn.png");
// 等价:await ctx.findTemplate("claim_btn");7.7 返回值
脚本的 start() 函数可以返回一个对象,作为执行结果:
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 查询;不满足条件时自动降级为单区域识别。
更容易触发复用的写法:
// 推荐:同一轮逻辑内连续做识别,不要在每次识别间 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);// 不推荐:在同一判断轮里多次 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慢调用阈值(毫秒)。