Files
bettergi-scripts-list/repo/js/FullyAutoAndSemiAutoTools/main.js
yan daf0577d6a fix(config): 修复加载路径层级配置验证逻辑
- 添加 finally 块确保 loadingLevel 最小值为 2
- 防止配置错误导致的加载层级异常问题
2026-01-16 23:19:35 +08:00

1384 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let manifest_json = "manifest.json";
let manifest = undefined
let configSettings = undefined
const auto = {
semi: false,//半自动
run: false,//运行
skip: false,//跳过
key: "",
}
const dev = {
isDebug: false,
debug: undefined,
}
const cd = {
open: false,
http_api: undefined,
}
const pathingName = "pathing"
let loadingLevel = 2
// const pathAsMap = new Map([])
// const pathRunMap = new Map([])
const needRunMap = new Map([])
const PATHING_ALL = new Array({level: 0, name: `${pathingName}`, parent_name: "", child_names: []})
let settingsNameList = new Array()
const settingsNameAsList = new Array()
let PATH_JSON_LIST = new Array()
const config_root = 'config'
const json_path_name = {
RecordText: `${config_root}\\record.json`,
RecordPathText: `${config_root}\\PathRecord.json`,
uidSettingsJson: `${config_root}\\uidSettings.json`,
cdPath: `${config_root}\\cd-${pathingName}.json`,
SevenElement: `${config_root}\\SevenElement.json`,
}
// 定义记录文件的路径
// let RecordText = `${config_root}\\record.json`
// let RecordPathText = `${config_root}\\PathRecord.json`
let RecordList = new Array()
let RecordLast = {
uid: "",
data: undefined,
timestamp: 0,
paths: new Set(), // 记录路径
errorPaths: new Set(),
groupPaths: new Set(),
}
const Record = {
uid: "",
data: undefined,
timestamp: 0,
paths: new Set(), // 记录路径
errorPaths: new Set(), // 记录错误路径
groupPaths: new Set(), // 记录分组路径
}
let RecordPath = {
uid: "",
paths: new Set(), // 记录路径
//{timestamp,path}
}
const config_list = {
black: [],
white: [],
}
const SevenElement = {
SevenElements: ['矿物', '火', '水', '风', '雷', '草', '冰', '岩'],
SevenElementsMap: new Map([
['矿物', ['夜泊石', '石珀', '清水玉', '万相石', '矿物']],
['火', []],
['水', ['海露花']],
['风', ['蒲公英籽']],
['雷', ['琉鳞石', '绯樱绣球']],
['草', []],
['冰', []],
['岩', []],
]),
}
const team = {
current: undefined,
fight: false,
fightName: settings.team_fight,
fightKeys: ['锄地专区', "敌人与魔物"],
SevenElements: settings.team_seven_elements ? settings.team_seven_elements.split(',').map(item => item.trim()) : [],
}
const timeType = Object.freeze({
hours: 'hours',//小时
cron: 'cron',//cron表达式
// 添加反向映射(可选)
fromValue(value) {
return Object.keys(this).find(key => this[key] === value);
}
});
/**
* 实时任务处理函数
* @param {boolean} is_common - 是否为通用任务标志
* @returns {void} 无返回值
*/
async function realTimeMissions(is_common = true) {
let real_time_missions = settings.real_time_missions // 从设置中获取实时任务列表
if (!is_common) { // 处理非通用任务
if (real_time_missions.includes("自动战斗")) {
// await dispatcher.runAutoFightTask(new AutoFightParam());
await dispatcher.runTask(new SoloTask("AutoFight")); // 执行自动战斗任务
}
return // 非通用任务处理完毕后直接返回
}
// 处理通用任务
if (real_time_missions.includes("自动对话")) {
dispatcher.addTrigger(new RealtimeTimer("AutoSkip")); // 添加自动对话触发器
}
if (real_time_missions.includes("自动拾取")) {
// 启用自动拾取的实时任务
dispatcher.addTrigger(new RealtimeTimer("AutoPick")); // 添加自动拾取触发器
}
}
async function init() {
let settingsConfig = await initSettings();
let utils = [
"cron",
"SwitchTeam",
"uid",
]
for (let util of utils) {
eval(file.readTextSync(`utils/${util}.js`));
}
if (manifest.key !== settings.key) {
let message = "密钥不匹配";
if (settings.key) {
message += ",脚本可能存在升级 密钥已经改变 请查看文档功能后重新获取密钥"
}
throw new Error(message)
}
auto.semi = settings.mode === "半自动"
if (auto.semi) {
auto.run = settings.auto_semi_key_mode === "继续运行"
auto.skip = settings.auto_semi_key_mode === "跳过"
auto.key = settings.auto_key
// AUTO_STOP = (AUTO_STOP) ? AUTO_STOP : settings.autoStop
// AUTO_SKIP = (AUTO_SKIP) ? AUTO_SKIP : settings.autoSkip
//
// auto.run = (auto.run) ? auto.run : settings.autoStop
// auto.skip = (auto.skip) ? auto.skip : settings.autoSkip
if (!auto.key) {
throw new Error(settings.mode + "模式下必须开启快捷键设置")
}
}
dev.debug = (dev.debug) ? dev.debug : settings.debug
dev.isDebug = settings.is_debug
cd.open = (cd.open) ? cd.open : settings.cd_open
cd.http_api = (cd.http_api) ? cd.http_api : settings.http_api
config_list.black = settings.config_black_list ? settings.config_black_list.split(",") : []
config_list.white = settings.config_white_list ? settings.config_white_list.split(",") : []
if (!file.IsFolder(`${pathingName}`)) {
let batFile = "SymLink.bat";
log.error("{0}文件夹不存在请在BetterGI中右键点击本脚本选择{1}。然后双击脚本目录下的{2}文件以创建文件夹链接", `${pathingName}`, "打开所在目录", batFile);
return false;
}
try {
let parse = JSON.parse(file.readTextSync(json_path_name.SevenElement));
if (parse) {
parse?.sort((a, b) => a.level - b.level)
SevenElement.SevenElements = Array.from(new Set(SevenElement.SevenElements.concat(parse.map(item => item.name))))
parse.forEach(item => {
const name = item.name
let value = item.value
if (SevenElement.SevenElementsMap.has(name)) {
value = Array.from(new Set(SevenElement.SevenElementsMap.get(name).concat(value)))
}
SevenElement.SevenElementsMap.set(name, value)
})
}
} catch (e) {
log.warn("[SevenElement]初始化失败error:{0}", e.message)
}
//记录初始化
await initRecord();
// 读取现有配置并合并
let uidSettingsMap = new Map()
const uidSettingsJson = json_path_name.uidSettingsJson;
try {
const existingData = JSON.parse(file.readTextSync(uidSettingsJson))
uidSettingsMap = new Map(existingData)
} catch (e) {
// 文件不存在时使用空Map
log.debug("配置文件不存在,将创建新的");
}
let levelName = "treeLevel"
async function refreshALL() {
let level = 0
const parent_level = level + 1
// 获取当前路径下的所有文件/文件夹
let pathSyncList = file.readPathSync(`${PATHING_ALL[level].name}`);
log.debug("{0}文件夹下有{1}个文件/文件夹", `${pathingName}`, pathSyncList.length);
let settingsList = settingsConfig
let parentJson = {
name: `${levelName}_${level}_${level}`,
type: "multi-checkbox",
label: `选择要执行的${parent_level}级路径`,
options: []
}
for (const element of pathSyncList) {
// log.warn("element={0}", element)
parentJson.options.push(element.replace(`${pathingName}\\`, ""))
}
await addUniquePath({level: level, name: `${pathingName}`, parent_name: '', child_names: parentJson.options})
let treePathList = await readPaths(`${pathingName}`)
await debugKey('log-treePathList.json', JSON.stringify(treePathList))
let pathJsonList = await treeToList(treePathList)
PATH_JSON_LIST = pathJsonList
// 预处理黑白名单数组移除空字符串并trim
const processedBlackList = config_list.black
.map(item => item.trim())
.filter(item => item !== "");
const processedWhiteList = config_list.white
.map(item => item.trim())
.filter(item => item !== "");
for (const element of pathJsonList) {
const pathRun = element.path
// 检查路径是否被允许
const isBlacklisted = processedBlackList.some(item => pathRun.includes(item));
const isWhitelisted = processedWhiteList.some(item => pathRun.includes(item));
if (isBlacklisted && !isWhitelisted) {
continue;
}
//方案1
try {
loadingLevel = parseInt(settings.loading_level)
} catch (e) {
log.warn("配置 {0} 错误,将使用默认值{0}", "加载路径层级", loadingLevel)
} finally {
//
loadingLevel = loadingLevel < 1 ? 2 : loadingLevel
}
// 优化版本
for (let i = 0; i < loadingLevel; i++) {
const currentLevel = parent_level + 1 + i;
const parentLevel = parent_level;
const currentName = getChildFolderNameFromRoot(pathRun, currentLevel);
const childName = getChildFolderNameFromRoot(pathRun, currentLevel + 1);
// 检查当前层级是否存在
if (!currentName) {
break; // 没有当前层级,停止处理
}
// 过滤JSON文件
const filteredChildName = childName?.endsWith(".json") ? undefined : childName;
// 获取父级名称用于建立层级关系
const parentName = getChildFolderNameFromRoot(pathRun, parentLevel);
await addUniquePath({
level: parent_level + i, // 存储到目标层级
name: currentName, // 当前层级名称
parent_name: parentName, // 父级名称
child_names: filteredChildName ? [filteredChildName] : []
});
}
//方案2
/* const level_parent_name = getChildFolderNameFromRoot(pathRun, parent_level);
const level1_name = getChildFolderNameFromRoot(pathRun, parent_level + 1);
let level2_name = getChildFolderNameFromRoot(pathRun, parent_level + 1 + 1);
let level3_name = getChildFolderNameFromRoot(pathRun, parent_level + 1 + 2);
if (level2_name.endsWith(".json")) {
level2_name = undefined
}
if (level3_name.endsWith(".json")) {
level3_name = undefined
}
//存储 2 级
await addUniquePath({
level: parent_level,
name: level1_name,
parent_name: level_parent_name,
child_names: level2_name ? [level2_name] : []
})
await addUniquePath({
level: parent_level + 1,
name: level2_name,
parent_name: level1_name,
child_names: level3_name ? [level3_name] : []
})*/
}
// 正确的排序方式
PATHING_ALL.sort((a, b) => {
// 首先按 level 排序
if (a.level !== b.level) {
return a.level - b.level;
}
if (a.parent_name !== b.parent_name) {
return a.parent_name.localeCompare(b.parent_name);
}
// level 相同时按 name 排序
return a.name.localeCompare(b.name);
});
await debugKey('log-PATHING_ALL.json', JSON.stringify(PATHING_ALL))
const groupLevel = groupByLevel(PATHING_ALL);
// const initLength = settingsList.length
let parentNameLast = undefined
// let parentNameNow = undefined
const line = 30
const br = `${"=".repeat(line)}\n`
let idx = 0
groupLevel.filter(list => list.length > 0).forEach(
(list) => {
let i = 0
list.filter(item => item && item.child_names && item.child_names.length > 0).forEach(item => {
const name = `${levelName}_${item.level}_${i}`
let prefix = ''
if (item.parent_name !== parentNameLast) {
parentNameLast = item.parent_name;
let b = (line - item.parent_name.length) % 2 === 0;
const localLine = b ? ((line - item.parent_name.length) / 2) : (Math.ceil((line - item.parent_name.length) / 2))
prefix = br + `${"=".repeat(localLine)}${item.parent_name}${"=".repeat(localLine)}\n` + br
}
// const p = idx === 0 ? "【地图追踪】\n" : `${prefix}[${item.parent_name}-${item.name}]\n`
const p = `${prefix}[${item.name}]\n`
idx++
let leveJson = {
name: `${name}`,
type: "multi-checkbox",
label: `${p}选择要执行的${item.level + 1}级路径`,
options: []
}
// leveJson.options = leveJson.options.concat(item.child_names)
leveJson.options = [...item.child_names]
if (leveJson.options && leveJson.options.length > 0) {
settingsNameAsList.push({
settings_name: name,
settings_as_name: item.name
})
settingsNameList.push(name)
const existingIndex = settingsList.findIndex(item => item.name === leveJson.name);
if (existingIndex !== -1) {
// 替换已存在的配置项
settingsList[existingIndex] = leveJson;
} else {
if (item.parent_name !== parentNameLast) {
settingsList.push({type: "separator"})
}
// 添加新的配置项
settingsList.push(leveJson);
}
i++
}
})
}
)
level++
settingsList.filter(
item => item.name === 'key'
).forEach(item => {
// 刷新settings自动设置密钥
item.default = manifest.key
})
// 更新当前用户的配置
uidSettingsMap.set(Record.uid, settingsList)
// 安全写入配置文件
try {
file.writeTextSync(uidSettingsJson, JSON.stringify([...uidSettingsMap]))
log.debug("用户配置已保存: {uid}", Record.uid)
} catch (error) {
log.error("保存用户配置失败: {error}", error.message)
}
file.writeTextSync(manifest.settings_ui, JSON.stringify(settingsList))
}
//总控
//刷新settings
if (settings.config_run === "刷新") {
await refreshALL();
} else if (settings.config_run === "加载") {
//直接从配置文件中加载对应账号的配置
let uidSettings = uidSettingsMap.get(Record.uid);
if (uidSettings) {
try {
file.writeTextSync(manifest.settings_ui, JSON.stringify(uidSettings))
} catch (e) {
log.error("加载用户配置失败: {error}", e.message)
}
}
configSettings = await initSettings()
settingsNameList = settingsNameList.concat(await getMultiCheckboxMap().then(map => {
return map.keys().filter(key => key.startsWith(levelName))
}))
} else
// 初始化needRunMap
if (settings.config_run === "执行") {
const cdPath = json_path_name.cdPath;
const timeJson = (!cd.open) ? new Set() : new Set(JSON.parse(file.readTextSync(cdPath)).sort(
(a, b) => b.level - a.level
))
// for (let key of pathAsMap.keys()) {
// const multiCheckbox = await getValueByMultiCheckboxName(pathAsMap.get(key));
// needRunMap.set(key, multiCheckbox)
// }
for (const settingsName of settingsNameList) {
// let multi = await getValueByMultiCheckboxName(settingsName);
const multiJson = await getJsonByMultiCheckboxName(settingsName)
const label = getBracketContent(multiJson.label)
let multi = multiJson.options
const settingsAsName = settingsNameAsList.find(item => item.settings_name === settingsName)
let list = PATH_JSON_LIST.filter(item =>
multi.some(element => item.path.includes(`\\${element}\\`) && item.path.includes(`\\${label}\\`))
).map(item => {
// 找到匹配的元素并填充到 selected 字段
const matchedElement = multi.find(element => item.path.includes(`\\${element}\\`) && item.path.includes(`\\${label}\\`));
return {name: item.name, parent_name: item.parent_name, selected: matchedElement || "", path: item.path}
});
// 1. 预处理:将 Set/Map 转为数组,避免循环内重复转换
const recordPaths = Array.from(RecordPath.paths);
const timeConfigs = Array.from(timeJson);
const timeFilter = list.filter(item => {
// 2. 查找匹配的配置项 (假设这是必须的条件)
// 注意:这里保留了原有的 includes 逻辑,但在实际业务中建议评估是否需要精确匹配
const timeConfig = timeConfigs.find(e => item.path.includes(`\\${e.name}\\`));
if (!timeConfig) return false;
// 3. 查找匹配的历史记录
// 同样保留了 includes 逻辑
const matchedRecord = recordPaths.find(element => element.path.includes(item.path));
// 4. 如果没有记录,或者记录中没有时间戳,则跳过
if (!matchedRecord || !matchedRecord.timestamp) return false;
const {timestamp, value} = matchedRecord;
const now = Date.now();
// 5. 根据配置的类型进行时间判断
if (timeConfig.type) {
switch (timeType.fromValue(timeConfig.type)) {
case timeType.hours:
const timeDifference = getTimeDifference(timestamp, now);
return timeDifference.total.hours >= value;
case timeType.cron:
const nextCronTimestamp = cronUtil.getNextCronTimestamp(`${value}`, timestamp, now, cd.http_api);
// if (!nextCronTimestamp) {
// log.error(`cron表达式解析失败: {value}`, value)
// throw new Error(`cron表达式解析失败: ${value}`)
// }
if (!nextCronTimestamp) return false;
return now >= nextCronTimestamp;
default:
return false;
}
}
return false;
});
if (timeFilter?.length > 0) {
//移除CD
list = Array.from(new Set(list).difference(new Set(timeFilter)))
}
if (list?.length > 0) {
needRunMap.set(settingsAsName.settings_as_name, {
paths: list,
as_name: settingsAsName.settings_as_name,
name: settingsAsName.settings_name
})
}
log.debug(`[CD]{0}[CD]`, JSON.stringify([...timeFilter]))
log.debug(`[RUN]{0}[RUN]`, JSON.stringify([...list]))
}
// 启用自动拾取的实时任务,并配置成启用急速拾取模式
// dispatcher.addTrigger(new RealtimeTimer("AutoPick"));
await realTimeMissions()
}
return true
}
(async function () {
try {
if (await init()) {
await main()
}
} finally {
saveRecord();
}
})()
async function main() {
let lastRunMap = new Map()
function chooseBestRun() {
if (settings.choose_best && RecordLast.paths.size > 0) {
// 由于在迭代过程中删除元素会影响迭代,先收集要删除的键
const keysToDelete = [];
// 优先跑上次没跑过的路径
// 使用 Set 提高性能
const lastListSet = new Set([...RecordLast.paths]);
for (const [key, one] of needRunMap.entries()) {
// 检查当前任务的路径是否都不在上次执行的路径中
const allPathsInLast = one.paths.every(pathObj => lastListSet.has(pathObj.path));
if (!allPathsInLast) {
lastRunMap.set(key, one);
keysToDelete.push(key);
}
}
// 然后批量删除
for (const key of keysToDelete) {
needRunMap.delete(key);
}
}
}
chooseBestRun();
if (needRunMap.size > 0) {
await runMap(needRunMap)
}
if (lastRunMap.size > 0) {
await runMap(lastRunMap)
}
if (needRunMap.size <= 0 && lastRunMap.size <= 0) {
log.info(`设置目录{0}完成`, "刷新")
}
// log.info(`[{mode}] path==>{path},请按下{key}以继续执行[${manifest.name} JS]`, settings.mode, "path", AUTO_STOP)
// await keyMousePressStart(AUTO_STOP);
// log.info(`[{mode}] path==>{path},请按下{key}以继续执行[${manifest.name} JS]`, settings.mode, "path", AUTO_STOP)
}
/**
* 保存记录路径的函数
* 该函数将RecordPath对象中的Set类型数据转换为数组后保存到文件
*/
async function saveRecordPaths() {
// 保存前将 Set 转换为数组因为JSON不支持Set类型
// 创建一个新的记录对象,包含原始记录的所有属性
const recordToSave = {
// 使用展开运算符复制Record对象的所有属性保持其他数据不变
...RecordPath,
// 处理 paths 数组
paths: (() => {
// 1. 使用 Map 来辅助去重Map 的 key 是 pathvalue 是完整的 item 对象
const pathMap = new Map();
// 假设 RecordPath.paths 是一个 Set先转为数组进行遍历
[...RecordPath.paths].forEach(item => {
// 获取当前项的路径字符串
const currentPath = item.path;
// 检查 Map 中是否已经存在该路径
if (pathMap.has(currentPath)) {
// 如果存在,比较时间戳
const existingItem = pathMap.get(currentPath);
// 如果当前项的时间戳比已存在的大,则更新 Map 中的值
if (item.timestamp > existingItem.timestamp) {
pathMap.set(currentPath, item);
}
} else {
// 如果不存在,直接存入 Map
pathMap.set(currentPath, item);
}
});
// 2. 将 Map 中的值(去重后的对象数组)转换回我们需要的格式
return Array.from(pathMap.values()).map(item => ({
timestamp: item.timestamp,
path: item.path
}));
})()
};
// 将记录列表转换为JSON字符串并同步写入文件
file.writeTextSync(json_path_name.RecordText, JSON.stringify(recordToSave))
}
/**
* 保存当前记录到记录列表并同步到文件
* 该函数在保存前会将Set类型的数据转换为数组格式确保JSON序列化正常进行
*/
function saveRecord() {
// 保存前将 Set 转换为数组
// 创建一个新的记录对象,包含原始记录的所有属性
const recordToSave = {
// 使用展开运算符复制Record对象的所有属性
...Record,
// 将paths Set转换为数组
paths: [...Record.paths],
// 将errorPaths Set转换为数组
errorPaths: [...Record.errorPaths],
// 将groupPaths Set转换为数组并对每个元素进行特殊处理
groupPaths: [...Record.groupPaths].map(item => ({
// 保留name属性
name: item.name,
// 将item中的paths Set转换为数组
paths: [...item.paths]
}))
};
// 将处理后的记录添加到记录列表
RecordList.push(recordToSave)
// 将记录列表转换为JSON字符串并同步写入文件
file.writeTextSync(json_path_name.RecordText, JSON.stringify(RecordList))
}
/**
* 计算两个时间之间的差值并返回指定格式的JSON
* @param {number|Date} startTime - 开始时间时间戳或Date对象
* @param {number|Date} endTime - 结束时间时间戳或Date对象
* @returns {Object} diff_json - 包含info和total的对象
*/
function getTimeDifference(startTime, endTime) {
// 确保输入是时间戳
const start = typeof startTime === 'object' ? startTime.getTime() : startTime;
const end = typeof endTime === 'object' ? endTime.getTime() : endTime;
// 计算总差值(毫秒)
const diffMs = Math.abs(end - start);
// 计算总时间(小数)
const totalSeconds = diffMs / 1000;
const totalMinutes = totalSeconds / 60;
const totalHours = totalSeconds / 3600;
// 计算info部分整数
const infoHours = Math.floor(totalHours % 24);
const remainingAfterHours = (totalHours % 24) - infoHours;
const infoMinutes = Math.floor(remainingAfterHours * 60);
const remainingAfterMinutes = (remainingAfterHours * 60) - infoMinutes;
const infoSeconds = Math.floor(remainingAfterMinutes * 60);
// 输出类似:
// {
// info: { hours: 1, minutes: 0, seconds: 0 },
// total: { hours: 1, minutes: 60, seconds: 3600 }
// }
const diff_json = {
info: {
hours: infoHours,
minutes: infoMinutes,
seconds: infoSeconds
},
total: {
hours: parseFloat(totalHours.toFixed(6)),
minutes: parseFloat(totalMinutes.toFixed(6)),
seconds: parseFloat(totalSeconds.toFixed(6))
}
};
return diff_json;
}
/**
* 初始化记录函数
* 该函数用于初始化一条新的记录包括设置UID、时间戳和调整后的日期数据
* 同时会检查记录列表中是否存在相同UID的最新记录并进行更新
*/
async function initRecord() {
// 设置记录的唯一标识符通过OCR技术获取
Record.uid = await uidUtil.ocrUID()
// 设置记录的时间戳为当前时间
Record.timestamp = Date.now()
// 获取并设置调整后的日期数据
Record.data = getAdjustedDate()
try {
// 尝试读取记录文件
// 读取后将数组转换回 Set处理特殊的数据结构
RecordList = JSON.parse(file.readTextSync(json_path_name.RecordText), (key, value) => {
// 处理普通路径集合
if (key === 'paths' || key === 'errorPaths') {
return new Set(value);
}
// 处理分组路径集合保持嵌套的Set结构
if (key === 'groupPaths') {
return new Set(value.map(item => ({
name: item.name,
paths: new Set(item.paths)
})));
}
return value;
});
} catch (e) {
// 如果读取文件出错,则忽略错误(可能是文件不存在或格式错误)
}
try {
// 尝试读取记录文件
// 读取后将数组转换回 Set处理特殊的数据结构
RecordPath = JSON.parse(file.readTextSync(json_path_name.RecordPathText), (key, value) => {
// 处理分组路径集合保持嵌套的Set结构
if (key === 'paths') {
return new Set(value.map(item => ({
timestamp: item.timestamp,
path: item.path
})));
}
return value;
}).find(item => item.uid === Record.uid)
} catch (e) {
// 如果读取文件出错,则忽略错误(可能是文件不存在或格式错误)
}
if (RecordPath?.uid) {
RecordPath.uid = Record.uid
}
if (RecordPath?.paths) {
RecordPath.paths = new Set()
}
// 如果记录列表不为空,则查找最新记录
if (RecordList.length > 0) {
// 最优解:一次遍历找到最新的记录
let latestRecord = undefined;
// 遍历记录列表查找相同UID且时间戳最大的记录
for (const item of RecordList) {
// 检查当前记录项的UID是否匹配并且是最新记录
if (item.uid === Record.uid &&
// 如果还没有找到记录,或者当前记录的时间戳比已找到的记录更新
(!latestRecord || item.timestamp > latestRecord.timestamp)) {
// 更新最新记录为当前项
latestRecord = item;
}
}
// 如果找到最新记录则更新RecordLast否则保持原有RecordLast的值
RecordLast = latestRecord ? latestRecord : RecordLast;
}
if (RecordLast.uid === Record.uid && Record.data === RecordLast.data) {
// 判断是否为同一天 合并跑过的数据
// 确保 RecordLast 的 Set 属性存在
if (!RecordLast.paths || !(RecordLast.paths instanceof Set)) {
RecordLast.paths = new Set();
}
if (!RecordLast.groupPaths || !(RecordLast.groupPaths instanceof Set)) {
RecordLast.groupPaths = new Set();
}
// 判断是否为同一天 合并跑过的数据
Record.paths = new Set([...Record.paths, ...RecordLast.paths])
Record.groupPaths = new Set([...Record.groupPaths, ...RecordLast.groupPaths])
// 删除RecordLast
const index = RecordList.indexOf(RecordLast);
if (index > -1) {
RecordList.splice(index, 1);
}
}
}
function getAdjustedDate() {
const now = new Date();
// 减去4小时4 * 60 * 60 * 1000 毫秒)
now.setHours(now.getHours() - 4);
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 解析日期字符串
function parseDate(dateString) {
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day); // month - 1 因为月份从0开始
}
// Record.groupPaths.add({
// name: "",
// paths:new Set()
// })
// const pathingALLSize = []
// 使用函数来添加唯一元素
// 优化后的函数
function addUniquePath(obj) {
const existingIndex = PATHING_ALL.findIndex(item =>
item.level === obj.level && item.name === obj.name
);
if (existingIndex === -1) {
PATHING_ALL.push(obj);
} else {
// 合并 child_names 数组,避免重复元素
const existingItem = PATHING_ALL[existingIndex];
const newChildren = obj.child_names || [];
// 使用 Set 去重并合并数组
const combinedChildren = [...new Set([
...(existingItem.child_names || []),
...newChildren
])];
existingItem.child_names = combinedChildren;
}
}
/**
* 初始化设置函数
* 从配置文件中读取设置信息并返回
* @returns {Object} 返回解析后的JSON设置对象
*/
async function initSettings() {
// 默认设置文件路径
let settings_ui = "settings.json";
try {
// 读取并解析manifest.json文件
manifest = manifest ? manifest : JSON.parse(file.readTextSync(manifest_json));
// 调试日志输出manifest内容
// log.debug("manifest={key}", manifest);
// 调试日志输出manifest中的settings_ui配置
log.debug("settings_ui={key}", manifest.settings_ui);
log.info(`|脚本名称:{name},版本:{version}`, manifest.name, manifest.version);
if (manifest.bgi_version) {
log.info(`|最小可执行BGI版本:{bgi_version}`, manifest.bgi_version);
}
log.info(`|脚本作者:{authors}\n`, manifest.authors.map(a => a.name).join(","));
// 更新settings_ui变量为manifest中指定的路径
settings_ui = manifest.settings_ui
} catch (error) {
// 捕获并记录可能的错误
log.warn("{error}", error.message);
}
// 读取并解析设置文件
const settingsJson = JSON.parse(file.readTextSync(settings_ui));
// 如果configSettings未定义则将其设置为解析后的设置对象
if (!configSettings) {
configSettings = settingsJson
}
// 调试日志:输出最终解析的设置对象
log.debug("settingsJson={key}", settingsJson);
// 返回设置对象
return settingsJson
}
/**
* 获取多复选框的映射表
* 该函数会从初始化的设置中提取所有类型为"multi-checkbox"的条目,
* 并将这些条目的名称和对应的选项值存储在一个Map对象中返回
* @returns {Promise<Map>} 返回一个Promise对象解析为包含多复选框配置的Map
*/
async function getMultiCheckboxMap() {
// 如果configSettings存在则使用它否则调用initSettings()函数获取
const settingsJson = configSettings ? configSettings : await initSettings();
// 创建一个新的Map对象用于存储多复选框的配置
// Map结构为: {名称: 选项数组}
let multiCheckboxMap = new Map();
// 遍历设置JSON中的每个条目
settingsJson.forEach((entry) => {
// 如果条目没有name属性或者类型不是"multi-checkbox",则跳过该条目
if (!entry.name || entry.type !== "multi-checkbox") return;
// 解构条目中的name和label属性便于后续使用
const {name, label} = entry;
// 获取当前name对应的设置值如果存在则转换为数组否则使用空数组
const options = settings[name] ? Array.from(settings[name]) : [];
// 记录调试信息,包含名称、标签、选项和选项数量
log.debug("name={key1},label={key2},options={key3},length={key4}", name, label, JSON.stringify(options), options.length);
// 将名称和对应的选项数组存入Map
// multiCheckboxMap.set(name, options);
multiCheckboxMap.set(name, {label: label, options: options});
})
// 返回包含多复选框配置的Map
return multiCheckboxMap
}
/**
* 根据多选框名称获取对应的JSON数据
* 该函数是一个异步函数,用于从复选框映射表中获取指定名称的值
* @param {string} name - 多选框的名称,用于在映射表中查找对应的值
* @returns {Promise<any>} 返回一个Promise解析后为找到的值如果未找到则返回undefined
*/
async function getJsonByMultiCheckboxName(name) {
// 获取复选框映射表,这是一个异步操作
let multiCheckboxMap = await getMultiCheckboxMap()
// 从映射表中获取并返回指定名称对应的值
return multiCheckboxMap.get(name)
}
/**
* 根据复选框组名称获取对应的值
* 这是一个异步函数,用于从复选框映射中获取指定名称的值
* @param {string} name - 复选框组的名称
* @returns {Promise<any>} 返回一个Promise解析为复选框组对应的值
*/
async function getValueByMultiCheckboxName(name) {
// 获取复选框映射表,这是一个异步操作
let multiCheckboxMap = await getMultiCheckboxMap()
// 从映射表中获取并返回指定名称对应的值
return multiCheckboxMap.get(name).options
}
/**
* 获取字符串中第一个方括号内的内容
* @param {string} str - 输入的字符串
* @returns {string} 返回第一个方括号内的内容,如果没有找到则返回空字符串
*/
function getBracketContent(str) {
// 使用正则表达式匹配第一个方括号及其中的内容
const match = str.match(/\[(.*?)\]/);
// 如果找到匹配项,返回第一个捕获组(即方括号内的内容),否则返回空字符串
return match ? match[1] : ''; // 找不到就回空字串
}
/**
* 调试按键函数,用于在开发者模式下暂停程序执行并等待特定按键
* @param {string} key - 需要按下的键
* @param {string} path - 调试信息保存的文件路径,默认为"debug.json"
* @param {string} json - 需要写入调试文件的内容,默认为空数组
* @returns {Promise<void>} - 异步函数,没有返回值
*/
async function debugKey(path = "debug.json", json = "", key = dev.debug) {
const p = "debug\\"
// 检查是否处于调试模式
if (dev.isDebug) {
log.warn("[{0}]正在写出{1}日志", '开发者模式', path)
// 将调试信息同步写入指定文件
file.writeTextSync(`${p}${path}`, json)
log.warn("[{0}]写出完成", '开发者模式')
// 输出等待按键的提示信息
log.warn("[{0}]请按下{1}继续执行", '开发者模式', key)
// 等待用户按下指定按键
await keyMousePressStart(key)
}
}
/**
* 监听指定按键的按下和释放事件
* @param {string} key - 需要监听的按键代码
* @param {boolean} enableSkip - 是否允许跳过监听默认为false
* @returns {Promise<Object>} 返回一个Promise对象解析为包含按键状态的对象
* - ok: boolean - 按键是否被完整按下并释放
* - skip: boolean - 是否跳过监听仅在enableSkip为true时有效
*/
async function keyMousePress(key, enableSkip = false) {
let press = {ok: false, skip: false} // 初始化返回对象,记录按键状态
const keyMouseHook = new KeyMouseHook() // 创建按键鼠标钩子实例
let keyDown = false // 记录按键是否被按下
let keyUp = false // 记录按键是否被释放
let down = false // 记录按键按下事件是否触发
let up = false // 记录按键释放事件是否触发
try {
// 注册按键按下事件处理函数
keyMouseHook.OnKeyDown(function (keyCode) {
log.debug("{keyCode}被按下", keyCode)
keyDown = (key === keyCode) // 检查是否是目标按键被按下
down = true // 标记按键按下事件已触发
});
// 注册按键释放事件处理函数
keyMouseHook.OnKeyUp(function (keyCode) {
log.debug("{keyCode}被释放", keyCode)
keyUp = (key === keyCode) // 检查是否是目标按键被释放
up = true // 标记按键释放事件已触发
});
// 循环等待直到按键被按下并释放,或者跳过条件满足
while (true) {
if (enableSkip) {
if (press.ok || press.skip) {
break;
}
} else if (press.ok) {
break;
}
press.ok = keyDown && keyUp //
press.skip = down && up //
await sleep(200) // 每次循环间隔200毫秒
}
return press
} finally {
//脚本结束前,记得释放资源!
keyMouseHook.dispose()
} // 释放按键钩子资源
}
/**
* 异步函数,用于检测特定按键的按下和释放事件
* @param {string|number} key - 需要检测的按键代码
* @returns {Promise<boolean>} 返回一个Promise解析为布尔值表示是否检测到按键的完整按下和释放过程
*/
async function keyMousePressStart(key) {
return (await keyMousePress(key)).ok
}
/**
* 执行指定路径的脚本文件
* @param {string} path - 要执行的脚本路径
*/
async function runPath(path) {
// 参数验证
if (!path || typeof path !== 'string') {
log.warn('无效的路径参数: {path}', path)
return
}
// 检查该路径是否已经在执行中
if (Record.paths.has(path)) {
log.info(`[{mode}] 路径已执行: {path},跳过执行`, settings.mode, path)
return
}
//检查战斗需求
try {
if (!team.fight) {
const one = JSON.parse(file.readTextSync(path))
if (one.info?.description?.includes("请配置好战斗策略")) {
log.warn(`[{mode}] 路径需要配置好战斗策略: {path},如已配置请忽略`, settings.mode, path)
team.fight = true
} else if (team.fightKeys.some(item => path.includes(`\\${item}\\`))) {
team.fight = true
}
}
} catch (error) {
log.error("检查战斗需求失败: {error}", error.message);
}
//切换队伍
if (team.fight) {
if (!team.fightName) {
log.error(`[{mode}] 路径需要配置好战斗策略: {path}`, settings.mode, path)
throw new Error(`路径需要配置好战斗策略: ` + path)
} else if (team.current !== team.fightName) {
log.info(`[{mode}] 检测到需要战斗,切换至{teamName}`, team.fightName);
const teamSwitch = await switchUtil.SwitchPartyMain(team.fightName);
if (teamSwitch) {
team.current = teamSwitch;
}
}
} else {
const entry = [...SevenElement.SevenElementsMap.entries()].find(([key, val]) => {
return val.some(item => path.includes(`\\${item}\\`));
});
if (entry) {
const [key, val] = entry;
const index = SevenElement.SevenElements.indexOf(key);
const teamName = team.SevenElements.length > index && index >= 0 ?
team.SevenElements[index] : undefined;
if (!teamName || teamName === "") {
log.debug(`[{mode}] 没有设置队伍: {teamName},跳过切换`, settings.mode, teamName);
} else if (team.current === teamName) {
log.debug(`[{mode}] 当前队伍为: {teamName},无需切换`, settings.mode, teamName);
} else {
log.info(`[{mode}] 检测到需要: {key},切换至{val}`, settings.mode, key, teamName);
const teamSwitch = await switchUtil.SwitchPartyMain(teamName);
if (teamSwitch) {
team.current = teamSwitch;
}
}
} else if (team.current !== team.fightName) {
const teamSwitch = await switchUtil.SwitchPartyMain(team.fightName);
if (teamSwitch) {
team.current = teamSwitch;
}
}
}
//切换队伍-end
try {
log.debug("开始执行路径: {path}", path)
await pathingScript.runFile(path)
if (team.fight) {
//启用战斗
// await dispatcher.runAutoFightTask(new AutoFightParam());
await realTimeMissions(false)
}
log.debug("路径执行完成: {path}", path)
RecordPath.paths.add({timestamp: Date.now(), path: path})
await saveRecordPaths()
Record.paths.add(path)
Record.errorPaths.delete(path)
} catch (error) {
Record.errorPaths.add(path)
log.error("路径执行失败: {path}, 错误: {error}", path, error.message)
} finally {
if (team.fight) {
// 重置战斗状态
team.fight = false
}
}
if (auto.semi && auto.run) {
log.warn(`[{mode}] 路径执行完成: {path}, 请按{key}继续`, settings.mode, path, auto.key)
await keyMousePressStart(auto.key)
}
}
/**
* 执行给定的路径列表
* @param {Array} list - 要执行的路径列表,默认为空数组
* @returns {Promise<void>}
*/
async function runList(list = []) {
// 参数验证
if (!Array.isArray(list)) {
log.warn('无效的路径列表参数: {list}', list);
return;
}
if (list.length === 0) {
log.debug('路径列表为空,跳过执行');
return;
}
log.debug(`[{mode}] 开始执行路径列表,共{count}个路径`, settings.mode, list.length);
// 遍历路径列表
for (let i = 0; i < list.length; i++) {
const onePath = list[i];
const path = onePath.path;
if (i === 0) {
log.info(`[{mode}] 开始执行[{1}-{2}]列表`, settings.mode, onePath.selected, onePath.parent_name);
}
log.debug('正在执行第{index}/{total}个路径: {path}', i + 1, list.length, path);
if (auto.semi && auto.skip) {
log.warn(`[{mode}] 按下{key}可跳过{0}执行,如不想跳过请按 空格 或 其他非功能键`, settings.mode, auto.key, path);
const skip = await keyMousePress(auto.key, auto.skip);
if (skip.skip) {
log.warn(`[{mode}] 按下{key}跳过{0}执行`, settings.mode, auto.key, path);
continue
}
}
try {
// 执行单个路径,并传入停止标识
await runPath(path);
} catch (error) {
log.error('执行路径列表中的路径失败: {path}, 错误: {error}', path, error.message);
continue; // 继续执行列表中的下一个路径
}
}
log.debug(`[{mode}] 路径列表执行完成`, settings.mode);
}
/**
* 遍历并执行Map中的任务
* @param {Map} map - 包含任务信息的Map对象默认为新的Map实例
* @returns {Promise<void>} - 异步执行,没有返回值
*/
async function runMap(map = new Map()) {
// 参数验证
if (!(map instanceof Map)) {
log.warn('无效的Map参数: {map}', map);
return;
}
if (map.size === 0) {
log.debug('任务Map为空跳过执行');
return;
}
log.info(`[{mode}] 开始执行任务Map共{count}个任务`, settings.mode, map.size);
// 遍历Map中的所有键
for (const [key, one] of map.entries()) {
if (one.paths.size <= 0) {
continue
}
try {
// 记录开始执行任务的日志信息
log.info(`[{0}] 开始执行[{1}]...`, settings.mode, one.as_name);
// 执行当前任务关联的路径列表
await runList(one.paths);
Record.groupPaths.add({
name: one.as_name,
paths: new Set(one.paths)
})
log.debug(`[{0}] 任务[{1}]执行完成`, settings.mode, one.as_name);
} catch (error) {
log.error(`[{0}] 任务[{1}]执行失败: {error}`, settings.mode, one.as_name, error.message);
continue; // 继续执行下一个任务
}
}
log.debug(`[{mode}] 任务Map执行完成`, settings.mode);
}
/**
* 获取上级文件夹名称(支持多级查找)
* @param {string} path - 完整路径
* @param {number} level - 向上查找的层级默认为1即直接上级
* @returns {string} 指定层级的文件夹名称,如果不存在则返回空字符串
*/
function getParentFolderName(path, level = 1) {
if (!path || typeof path !== 'string' || level < 1) {
return undefined;
}
// 统一处理路径分隔符
const normalizedPath = path.replace(/\\/g, '/');
// 移除末尾的斜杠
const trimmedPath = normalizedPath.replace(/\/$/, '');
// 按斜杠分割路径
const pathParts = trimmedPath.split('/').filter(part => part !== '');
// 检查是否有足够的层级
if (level >= pathParts.length) {
return undefined;
}
// 返回指定层级的上级目录名称
return pathParts[pathParts.length - level - 1];
}
/**
* 从根目录开始获取指定位置的文件夹名称(支持多级查找)
* @param {string} path - 完整路径
* @param {number} level - 从根开始向下的层级默认为1即第一个子目录
* @returns {string} 指定层级的文件夹名称如果不存在则返回undefined
*/
function getChildFolderNameFromRoot(path, level = 1) {
if (!path || typeof path !== 'string' || level < 1) {
return undefined;
}
// 统一处理路径分隔符
const normalizedPath = path.replace(/\\/g, '/');
// 移除开头的斜杠(如果有)
const trimmedPath = normalizedPath.replace(/^\/+/, '');
// 按斜杠分割路径
const pathParts = trimmedPath.split('/').filter(part => part !== '');
// 检查是否有足够的层级
if (level > pathParts.length) {
return undefined;
}
// 返回从根开始指定层级的目录名称level - 1 是因为数组索引从0开始
return pathParts[level - 1];
}
/**
* 按层级对列表项进行分组
* @param {Array} list - 包含层级信息的列表项数组
* @returns {Array} 返回一个嵌套数组,每个子数组包含对应层级的所有项
*/
function groupByLevel(list) {
// 找出最大层级数
const maxLevel = Math.max(...list.map(item => item.level));
// 创建嵌套数组结构
const result = [];
// 按层级分组
for (let level = 0; level <= maxLevel; level++) {
const levelItems = list.filter(item => item.level === level);
result.push(levelItems);
}
return result;
}
/**
* 递归读取指定路径下的文件和文件夹,构建树形结构
* @param {string} path - 要读取的初始路径
* @param {number} index - 当前层级的索引默认为0
* @param {string} isFileKey - 目标文件类型的后缀名,默认为".json"
* @param {boolean} treeStructure - 是否使用树状结构返回默认为true
* @returns {Promise<Array>} 返回包含文件和文件夹结构的数组
*/
async function readPaths(path, index = 0, isFileKey = ".json", treeStructure = true) {
let treeList = []; // 用于存储当前层级的文件和文件夹结构
// 获取当前路径下的所有文件/文件夹
let pathSyncList = file.readPathSync(path);
// 遍历当前路径下的所有文件和文件夹
for (const pathSync of pathSyncList) {
// 如果是目标文件类型(默认为.json
if (pathSync.endsWith(isFileKey)) {
// 如果是目标文件类型,添加到列表
let name = undefined;
let parentName = undefined;
// let path_let = pathSync;
let parentFolder = getParentFolderName(pathSync)
if (!parentFolder) {
throw new Error(`${pathSync}没有上级目录`)
}
// 获取父级目录路径(去除文件名)
if (parentFolder.includes("@")) {
// 包含@符号的情况:取@符号前的上级目录名
// let first = path_let.split("@")[0];
// first = first.substring(0, first.lastIndexOf("\\"));
name = getParentFolderName(pathSync, 2);
parentName = getParentFolderName(pathSync, 3);
}
// else if (pathSync.includes("挪德卡莱锄地小怪")) {
// // 特殊处理
// let first_te = path_let.split("挪德卡莱锄地小怪")[0];
// first_te = first_te.substring(0, first_te.lastIndexOf("\\"));
// name = first_te.substring(first_te.lastIndexOf("\\"), first_te.length);
// }
else {
name = parentFolder;
parentName = getParentFolderName(pathSync, 2);
}
// 根据 treeStructure 参数决定是否创建完整对象
if (treeStructure) {
treeList.push({
name: name,
parentName: parentName,
path: pathSync,
index: index + 1,
isRoot: false,
isFile: true,
child: []
});
} else {
// 如果不需要树状结构,只添加基本文件信息
treeList.push({
name: name,
path: pathSync,
isFile: true
});
}
} else if (file.IsFolder(pathSync)) {
// 如果是文件夹,根据 treeStructure 参数决定如何处理
if (treeStructure) {
// 如果需要树状结构,递归处理并保留文件夹信息
const childTreeList = await readPaths(pathSync, index + 1, isFileKey, treeStructure);
treeList.push({
name: undefined,
parentName: undefined,
path: pathSync,
index: index + 1,
isRoot: false,
isFile: false,
child: childTreeList
});
} else {
// 如果不需要树状结构,直接递归遍历子文件夹,只收集文件
const childTreeList = await readPaths(pathSync, index + 1, isFileKey, treeStructure);
treeList = treeList.concat(childTreeList); // 将子文件夹中的文件直接合并到当前列表
}
}
}
return treeList;
}
async function treeToList(treeList = []) {
let list = []
for (const element of treeList) {
const child = element.child
if (child && child.length > 0) {
list = list.concat(await treeToList(child))
}
// 如果是文件,添加到结果列表
if (element.isFile) {
list.push(element);
}
}
return list
}