mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-30 05:49:51 +08:00
539 lines
16 KiB
JavaScript
539 lines
16 KiB
JavaScript
import { __name } from "./rolldown-runtime.js";
|
||
|
||
//#region node_modules/@bettergi/utils/dist/workflow.js
|
||
/** 默认最大重试次数 */
|
||
const defaultMaxAttempts = 5;
|
||
/** 默认重试间隔(毫秒) */
|
||
const defaultRetryInterval = 1e3;
|
||
/**
|
||
* 等待直到条件满足或超时,期间执行重试操作
|
||
* @param condition 返回条件是否满足的函数
|
||
* @param retryAction 每次重试时执行的操作(可选)
|
||
* @param options 配置选项
|
||
* @returns - true 条件满足
|
||
* - false 达到最大重试次数
|
||
*/
|
||
const waitForAction = async (condition, retryAction, options) => {
|
||
const { maxAttempts = defaultMaxAttempts, retryInterval = defaultRetryInterval } = options || {};
|
||
for (let i = 0; i < maxAttempts; i++) {
|
||
if (i === 0 && condition()) return true;
|
||
await retryAction?.();
|
||
await sleep(retryInterval);
|
||
if (condition()) return true;
|
||
}
|
||
return false;
|
||
};
|
||
/**
|
||
* 等待某个区域出现,期间执行重试操作
|
||
* @param regionProvider 返回区域的函数
|
||
* @param retryAction 每次重试时执行的操作(可选)
|
||
* @param options 配置选项
|
||
* @returns - true 区域出现
|
||
* - false 达到最大重试次数
|
||
*/
|
||
const waitForRegionAppear = async (regionProvider, retryAction, options) => {
|
||
return waitForAction(
|
||
() => {
|
||
const region = regionProvider();
|
||
return region != null && region.isExist();
|
||
},
|
||
retryAction,
|
||
options,
|
||
);
|
||
};
|
||
/**
|
||
* 等待某个区域消失,期间执行重试操作
|
||
* @param regionProvider 返回区域的函数
|
||
* @param retryAction 每次重试时执行的操作(可选)
|
||
* @param options 配置选项
|
||
* @returns - true 区域消失
|
||
* - false 达到最大重试次数
|
||
*/
|
||
const waitForRegionDisappear = async (regionProvider, retryAction, options) => {
|
||
return waitForAction(
|
||
() => {
|
||
const region = regionProvider();
|
||
return !region || !region.isExist();
|
||
},
|
||
retryAction,
|
||
options,
|
||
);
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/asserts.js
|
||
/**
|
||
* 断言某个区域即将出现,否则抛出异常
|
||
* @param regionProvider 返回区域的函数
|
||
* @param message 错误信息
|
||
* @param retryAction 每次重试时执行的操作(可选)
|
||
* @param options 配置选项
|
||
*/
|
||
const assertRegionAppearing = async (regionProvider, message, retryAction, options) => {
|
||
if (!(await waitForRegionAppear(regionProvider, retryAction, options))) throw new Error(message);
|
||
};
|
||
/**
|
||
* 断言某个区域即将消失,否则抛出异常
|
||
* @param regionProvider 返回区域的函数
|
||
* @param message 错误信息
|
||
* @param retryAction 每次重试时执行的操作(可选)
|
||
* @param options 配置选项
|
||
*/
|
||
const assertRegionDisappearing = async (regionProvider, message, retryAction, options) => {
|
||
if (!(await waitForRegionDisappear(regionProvider, retryAction, options)))
|
||
throw new Error(message);
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/exception.js
|
||
/**
|
||
* 获取错误信息字符串
|
||
* @param err 异常对象
|
||
* @returns 错误信息字符串
|
||
*/
|
||
const getErrorMessage = (err) => {
|
||
if (err && "message" in err && typeof err.message === "string") return err.message;
|
||
return err && typeof err === "object" ? JSON.stringify(err) : "Unknown error";
|
||
};
|
||
/**
|
||
* 判断是否为主机异常
|
||
* @param err 异常对象
|
||
*/
|
||
const isHostException = (err) => {
|
||
return err && "hostException" in err;
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/mouse.js
|
||
/** 使用回放脚本模拟滚动 */
|
||
const simulateScroll = async (wheelDelta, times) => {
|
||
const script = {
|
||
macroEvents: Array(times).fill({
|
||
type: 6,
|
||
mouseX: 0,
|
||
mouseY: wheelDelta,
|
||
time: 0,
|
||
}),
|
||
info: {
|
||
name: "",
|
||
description: "",
|
||
x: 0,
|
||
y: 0,
|
||
width: 1920,
|
||
height: 1080,
|
||
recordDpi: 1.5,
|
||
},
|
||
};
|
||
await keyMouseScript.run(JSON.stringify(script));
|
||
};
|
||
/**
|
||
* 鼠标滚轮向下滚动指定高度
|
||
* @param height 滚动高度
|
||
* @param algorithm 自定义滚动算法函数,接收高度参数并返回滚动次数(默认算法为每18像素滚动一次)
|
||
*/
|
||
const mouseScrollDown = (height, algorithm = (h) => Math.floor(h / 17.9795)) => {
|
||
return simulateScroll(-120, algorithm(height));
|
||
};
|
||
/**
|
||
* 鼠标滚轮向下滚动指定行数
|
||
* @param lines 滚动行数
|
||
* @param lineHeight 行高(默认值为175像素)
|
||
*/
|
||
const mouseScrollDownLines = (lines, lineHeight = 175) => {
|
||
return mouseScrollDown(lines * lineHeight);
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/ocr.js
|
||
const scaleTo1080P = (n) => {
|
||
return genshin.scaleTo1080PRatio <= 1 ? n : Math.floor(n / genshin.scaleTo1080PRatio);
|
||
};
|
||
/**
|
||
* 在指定区域内搜索图片
|
||
* @param image 图片路径 或 图片Mat
|
||
* @param x 水平方向偏移量(像素)
|
||
* @param y 垂直方向偏移量(像素)
|
||
* @param w 宽度
|
||
* @param h 高度
|
||
* @param config 识别对象配置
|
||
* @returns 如果找到匹配的图片区域,则返回该区域,否则返回 undefined
|
||
*/
|
||
const findImageWithinBounds = (image, x, y, w, h, config = {}) => {
|
||
const ir = captureGameRegion();
|
||
try {
|
||
const mat = typeof image === "string" ? file.readImageMatSync(image) : image;
|
||
const ro = RecognitionObject.templateMatch(mat, x, y, w, h);
|
||
if (Object.keys(config).length > 0) Object.assign(ro, config) && ro.initTemplate();
|
||
const region = ir.find(ro);
|
||
return region.isExist() ? region : void 0;
|
||
} catch (err) {
|
||
log.warn(`${err.message || err}`);
|
||
} finally {
|
||
ir.dispose();
|
||
}
|
||
};
|
||
/**
|
||
* 在图像区域内查找第一个符合条件的识别区域
|
||
* @param ir 图像区域
|
||
* @param ro 识别对象
|
||
* @param predicate 筛选条件
|
||
* @returns 第一个符合条件的识别区域,未找到则返回 undefined
|
||
*/
|
||
const findFirstRegion = (ir, ro, predicate) => {
|
||
const candidates = ir.findMulti(ro);
|
||
for (let i = 0; i < candidates.count; i++) if (predicate(candidates[i])) return candidates[i];
|
||
};
|
||
/**
|
||
* 文本匹配
|
||
* @param text 待匹配文本
|
||
* @param searchText 待搜索文本
|
||
* @param options 搜索选项
|
||
* @returns 是否匹配
|
||
*/
|
||
const textMatch = (text, searchText, options) => {
|
||
const { ignoreCase = true, contains = false } = options || {};
|
||
text = ignoreCase ? text.toLowerCase() : text;
|
||
searchText = ignoreCase ? searchText.toLowerCase() : searchText;
|
||
return contains ? text.includes(searchText) : text === searchText;
|
||
};
|
||
/**
|
||
* 在指定区域内搜索文本
|
||
* @param text 待搜索文本
|
||
* @param x 水平方向偏移量(像素)
|
||
* @param y 垂直方向偏移量(像素)
|
||
* @param w 宽度
|
||
* @param h 高度
|
||
* @param options 搜索选项
|
||
* @param config 识别对象配置
|
||
* @returns 如果找到匹配的文本区域,则返回该区域,否则返回 undefined
|
||
*/
|
||
const findTextWithinBounds = (text, x, y, w, h, options, config = {}) => {
|
||
const ir = captureGameRegion();
|
||
try {
|
||
const ro = RecognitionObject.ocr(x, y, w, h);
|
||
if (Object.keys(config).length > 0) Object.assign(ro, config) && ro.initTemplate();
|
||
return findFirstRegion(ir, ro, (region) => {
|
||
return region.isExist() && textMatch(region.text, text, options);
|
||
});
|
||
} catch (err) {
|
||
log.warn(`${err.message || err}`);
|
||
} finally {
|
||
ir.dispose();
|
||
}
|
||
};
|
||
/**
|
||
* 在列表视图中滚动搜索区域
|
||
* @param condition 查找条件
|
||
* @param listView 列表视图参数
|
||
* @param retryOptions 重试选项
|
||
* @param threshold 列表视图变化匹配阈值(默认:0.9)
|
||
* @returns 如果找到匹配的区域,则返回该区域,否则返回 undefined
|
||
*/
|
||
const findWithinListView = async (condition, listView, retryOptions, threshold = 0.9) => {
|
||
const { x, y, w, h, lineHeight, scrollLines = 1, paddingX = 10, paddingY = 10 } = listView;
|
||
const { maxAttempts = 99, retryInterval = 1200 } = retryOptions || {};
|
||
const captureListViewRegion = () => captureGameRegion().deriveCrop(x, y, w, h);
|
||
const isReachedBottom = (() => {
|
||
let captured;
|
||
let lastCaptured;
|
||
return () => {
|
||
try {
|
||
captured = captureListViewRegion();
|
||
if (!lastCaptured) return false;
|
||
const lc = lastCaptured.deriveCrop(1, 1, lastCaptured.width - 2, lastCaptured.height - 2);
|
||
const ro = RecognitionObject.templateMatch(lc.srcMat);
|
||
ro.threshold = threshold;
|
||
ro.use3Channels = true;
|
||
ro.initTemplate();
|
||
return captured.find(ro).isExist();
|
||
} catch {
|
||
return true;
|
||
} finally {
|
||
lastCaptured = captured;
|
||
}
|
||
};
|
||
})();
|
||
let targetRegion;
|
||
await waitForAction(
|
||
() => {
|
||
targetRegion = condition(captureListViewRegion());
|
||
return targetRegion?.isExist() || isReachedBottom();
|
||
},
|
||
async () => {
|
||
moveMouseTo(x + w - paddingX, y + paddingY);
|
||
await sleep(200);
|
||
await mouseScrollDownLines(scrollLines, lineHeight);
|
||
},
|
||
{
|
||
maxAttempts,
|
||
retryInterval,
|
||
},
|
||
);
|
||
if (targetRegion?.isExist()) {
|
||
const { item1, item2 } = targetRegion.convertPositionToGameCaptureRegion(0, 0);
|
||
Object.assign(targetRegion, {
|
||
x: scaleTo1080P(item1),
|
||
y: scaleTo1080P(item2),
|
||
});
|
||
return targetRegion;
|
||
}
|
||
};
|
||
/**
|
||
* 在列表视图中滚动搜索文本
|
||
* @param text 待搜索文本
|
||
* @param listView 列表视图参数
|
||
* @param matchOptions 搜索选项
|
||
* @param retryOptions 重试选项
|
||
* @param config 识别对象配置
|
||
* @param threshold 列表视图变化匹配阈值(默认:0.9)
|
||
* @returns 如果找到匹配的文本区域,则返回该区域,否则返回 undefined
|
||
*/
|
||
const findTextWithinListView = async (
|
||
text,
|
||
listView,
|
||
matchOptions,
|
||
retryOptions,
|
||
config = {},
|
||
threshold = 0.9,
|
||
) => {
|
||
const ro = RecognitionObject.ocrThis;
|
||
if (Object.keys(config).length > 0) Object.assign(ro, config) && ro.initTemplate();
|
||
return findWithinListView(
|
||
(lvr) => {
|
||
return findFirstRegion(lvr, ro, (region) => {
|
||
return region.isExist() && textMatch(region.text, text, matchOptions);
|
||
});
|
||
},
|
||
listView,
|
||
retryOptions,
|
||
threshold,
|
||
);
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/misc.js
|
||
/**
|
||
* 深度合并多个对象
|
||
* @param objects 多个对象
|
||
* @returns 合并后的对象副本
|
||
*/
|
||
const deepMerge = (...objects) => {
|
||
const isPlainObject = (input) => input?.constructor === Object;
|
||
return objects.reduce((result, obj) => {
|
||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||
acc[key] =
|
||
isPlainObject(acc[key]) && isPlainObject(value) ? deepMerge(acc[key], value) : value;
|
||
return acc;
|
||
}, result);
|
||
}, {});
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/time.js
|
||
/**
|
||
* 获取下一个(含当日)凌晨4点的时间
|
||
*/
|
||
const getNextDay4AM = () => {
|
||
const now = /* @__PURE__ */ new Date();
|
||
const result = new Date(now);
|
||
result.setHours(4, 0, 0, 0);
|
||
const daysUntilNextDay = now.getHours() < 4 ? 0 : 1;
|
||
result.setDate(now.getDate() + daysUntilNextDay);
|
||
return result;
|
||
};
|
||
/**
|
||
* 获取下一个(含当日)周一凌晨4点的时间
|
||
*/
|
||
const getNextMonday4AM = () => {
|
||
const now = /* @__PURE__ */ new Date();
|
||
const result = new Date(now);
|
||
result.setHours(4, 0, 0, 0);
|
||
const currentDay = now.getDay();
|
||
const daysUntilNextMonday = currentDay === 1 && now.getHours() < 4 ? 0 : 8 - currentDay;
|
||
result.setDate(now.getDate() + daysUntilNextMonday);
|
||
return result;
|
||
};
|
||
/**
|
||
* 解析时长
|
||
* @param duration 时长(毫秒)
|
||
*/
|
||
const parseDuration = (duration) => {
|
||
return {
|
||
h: Math.floor(duration / 36e5),
|
||
m: Math.floor((duration % 36e5) / 6e4),
|
||
s: Math.floor((duration % 6e4) / 1e3),
|
||
ms: Math.floor(duration % 1e3),
|
||
};
|
||
};
|
||
/**
|
||
* 将时长转换为时钟字符串
|
||
* @param duration 时长(毫秒)
|
||
*/
|
||
const formatDurationAsClock = (duration) => {
|
||
return Object.values(parseDuration(duration))
|
||
.slice(0, 3)
|
||
.map((num) => String(num).padStart(2, "0"))
|
||
.join(":");
|
||
};
|
||
/**
|
||
* 将时长转换为可读格式
|
||
* @param duration 时长(毫秒)
|
||
*/
|
||
const formatDurationAsReadable = (duration) => {
|
||
return Object.entries(parseDuration(duration))
|
||
.filter(([, value]) => value > 0)
|
||
.map(([unit, value]) => `${value}${unit}`)
|
||
.join(" ");
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/progress.js
|
||
/** 进度追踪器 */
|
||
var ProgressTracker = class {
|
||
total = 0;
|
||
current = 0;
|
||
startTime = Date.now();
|
||
formatter;
|
||
interval;
|
||
lastPrintTime = 0;
|
||
constructor(total, config) {
|
||
const { formatter, interval = 3e3 } = config || {};
|
||
this.total = total;
|
||
this.formatter = formatter || this.defaultFormatter;
|
||
this.interval = interval;
|
||
}
|
||
defaultFormatter = (logger, message, progress) => {
|
||
logger(
|
||
"[🚧 {pct} ⏳ {eta}]: {msg}",
|
||
progress.formatted.percentage.padStart(6),
|
||
progress.current > 0 && progress.elapsed > 0 ? progress.formatted.remaining : "--:--:--",
|
||
message,
|
||
);
|
||
};
|
||
tick(options) {
|
||
const { increment = 1, message, force = false } = options || {};
|
||
this.current = Math.min(this.current + increment, this.total);
|
||
if (message) this.print(message, force);
|
||
return this.current === this.total;
|
||
}
|
||
complete(message) {
|
||
this.current = this.total;
|
||
this.print(message, true);
|
||
}
|
||
reset() {
|
||
this.current = 0;
|
||
this.startTime = Date.now();
|
||
this.lastPrintTime = 0;
|
||
}
|
||
print(message, force = false, logger = log.info) {
|
||
if (force || this.shouldPrint()) {
|
||
this.formatter(logger, message, this.getProgress());
|
||
this.printed();
|
||
}
|
||
}
|
||
shouldPrint() {
|
||
return Date.now() - this.lastPrintTime >= this.interval;
|
||
}
|
||
printed() {
|
||
this.lastPrintTime = Date.now();
|
||
}
|
||
getProgress() {
|
||
const percentage = this.current / this.total;
|
||
const elapsed = Date.now() - this.startTime;
|
||
const average = this.current > 0 ? elapsed / this.current : 0;
|
||
const remaining = (this.total - this.current) * average;
|
||
return {
|
||
current: this.current,
|
||
total: this.total,
|
||
percentage,
|
||
elapsed,
|
||
average,
|
||
remaining,
|
||
formatted: {
|
||
percentage: `${(percentage * 100).toFixed(1)}%`,
|
||
elapsed: formatDurationAsReadable(elapsed),
|
||
average: formatDurationAsReadable(average),
|
||
remaining: formatDurationAsClock(remaining),
|
||
},
|
||
};
|
||
}
|
||
};
|
||
|
||
//#endregion
|
||
//#region node_modules/@bettergi/utils/dist/store.js
|
||
/**
|
||
* 创建一个持久化存储对象,用于管理应用状态数据
|
||
* 该函数会创建一个代理对象,对该对象的所有属性的修改都会自动同步到相应的JSON文件(脚本的 `store` 目录下)中。
|
||
* 支持深层嵌套对象的代理。
|
||
* @param name 存储对象的名称,将作为文件名(不包扩展名)
|
||
*/
|
||
const useStore = (name) => {
|
||
const filePath = `store/${name}.json`;
|
||
const storeData = (() => {
|
||
try {
|
||
if (
|
||
![...file.readPathSync("store")].map((path) => path.replace(/\\/g, "/")).includes(filePath)
|
||
)
|
||
throw new Error("File does not exist");
|
||
const text = file.readTextSync(filePath);
|
||
return JSON.parse(text);
|
||
} catch {
|
||
return {};
|
||
}
|
||
})();
|
||
const createProxy = (targetObject, parentPath = []) => {
|
||
if (typeof targetObject !== "object" || targetObject === null) return targetObject;
|
||
return new Proxy(targetObject, {
|
||
get: (target, key) => {
|
||
const value = Reflect.get(target, key);
|
||
return typeof value === "object" && value !== null
|
||
? createProxy(value, [...parentPath, key])
|
||
: value;
|
||
},
|
||
set: (target, key, value) => {
|
||
const success = Reflect.set(target, key, value);
|
||
if (success)
|
||
Promise.resolve().then(() => {
|
||
file.writeTextSync(filePath, JSON.stringify(storeData, null, 2));
|
||
});
|
||
return success;
|
||
},
|
||
deleteProperty: (target, key) => {
|
||
const success = Reflect.deleteProperty(target, key);
|
||
if (success)
|
||
Promise.resolve().then(() => {
|
||
file.writeTextSync(filePath, JSON.stringify(storeData, null, 2));
|
||
});
|
||
return success;
|
||
},
|
||
});
|
||
};
|
||
return createProxy(storeData);
|
||
};
|
||
/**
|
||
* 创建一个带有默认值的持久化存储对象,用于管理应用状态数据
|
||
* @param name 存储对象的名称,将作为文件名(不包扩展名)
|
||
* @param defaults 默认值数据对象
|
||
*/
|
||
const useStoreWithDefaults = (name, defaults) => {
|
||
const newStore = useStore(name);
|
||
Object.assign(newStore, deepMerge(defaults, newStore));
|
||
return newStore;
|
||
};
|
||
|
||
//#endregion
|
||
export {
|
||
ProgressTracker,
|
||
assertRegionAppearing,
|
||
assertRegionDisappearing,
|
||
findImageWithinBounds,
|
||
findTextWithinBounds,
|
||
findTextWithinListView,
|
||
getErrorMessage,
|
||
getNextDay4AM,
|
||
getNextMonday4AM,
|
||
isHostException,
|
||
useStoreWithDefaults,
|
||
waitForAction,
|
||
};
|