mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-26 05:09:52 +08:00
- 添加 settingsRefreshList 用于管理设置项刷新 - 修改 addUniquePath 函数支持传入列表参数 - 修复 parentLevel 计算逻辑,确保层级正确对应 - 更新排序逻辑,使用 path 字段替代 parent_name 进行比较 - 重构 readPaths 函数,实现完整的树形结构构建 - 添加 getFileOrFolderName 和 getParentFolderName 辅助函数 - 优化 settingsList 的合并和去重处理 - 统一使用 children 字段替代 child 字段
1523 lines
59 KiB
JavaScript
1523 lines
59 KiB
JavaScript
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 settingsRefreshList = []
|
||
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}\\`, ""))
|
||
}
|
||
settingsRefreshList.push({type: "separator"})
|
||
await addUniquePath({
|
||
level: level,
|
||
name: `${pathingName}`,
|
||
parent_name: '',
|
||
child_names: parentJson.options
|
||
}, settingsRefreshList)
|
||
|
||
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 + i;
|
||
|
||
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: parentLevel, // 存储到目标层级 属于目标层级
|
||
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;
|
||
}
|
||
const pathA = a?.path || '';
|
||
const pathB = b?.path || '';
|
||
// if (a.parent_name !== b.parent_name) {
|
||
// return a.parent_name.localeCompare(b.parent_name);
|
||
// }
|
||
// level 相同时按 name 排序
|
||
return pathA.localeCompare(pathB);
|
||
});
|
||
|
||
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
|
||
const settingsSortMap = new Map([])
|
||
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 = ''
|
||
log.debug(`[{2}]Last{0},Current{1},Name{5}`, "比对", parentNameLast, item.parent_name, item.name)
|
||
const isCommonLastAndCurrent = item.parent_name !== parentNameLast;
|
||
if (isCommonLastAndCurrent) {
|
||
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 = settingsRefreshList.findIndex(item => item.name === leveJson.name);
|
||
if (existingIndex !== -1) {
|
||
// 替换已存在的配置项
|
||
settingsRefreshList[existingIndex] = leveJson;
|
||
} else {
|
||
if (isCommonLastAndCurrent) {
|
||
settingsRefreshList.push({type: "separator"})
|
||
}
|
||
parentNameLast = item.parent_name;
|
||
|
||
// 添加新的配置项
|
||
settingsRefreshList.push(leveJson);
|
||
}
|
||
i++
|
||
}
|
||
})
|
||
}
|
||
)
|
||
//settingsRefreshList 二级排序 todo:
|
||
level++
|
||
settingsList = Array.from(new Set(settingsList.concat(settingsRefreshList)))
|
||
|
||
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 是 path,value 是完整的 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, list = PATHING_ALL) {
|
||
const existingIndex = list.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,
|
||
// children: []
|
||
// });
|
||
// } 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,
|
||
// children: childTreeList
|
||
// });
|
||
// } else {
|
||
// // 如果不需要树状结构,直接递归遍历子文件夹,只收集文件
|
||
// const childTreeList = await readPaths(pathSync, index + 1, isFileKey, treeStructure);
|
||
// treeList = treeList.concat(childTreeList); // 将子文件夹中的文件直接合并到当前列表
|
||
// }
|
||
// }
|
||
// }
|
||
//
|
||
// return treeList;
|
||
// }
|
||
|
||
/**
|
||
* 递归读取指定路径下的文件和文件夹,构建树形结构
|
||
* @param {string} path - 要读取的初始路径
|
||
* @param {number} level - 当前层级(从 0 开始),默认为 0
|
||
* @param {string} parentId - 父节点的 id(完整路径),根节点为 null
|
||
* @param {string[]} fullPathNames - 从根到当前节点的名称路径数组
|
||
* @param {string} isFileKey - 目标文件类型的后缀名,默认为 ".json"
|
||
* @param {boolean} treeStructure - 是否返回树状结构(true: 树 / false: 只收集文件平铺)
|
||
* @returns {Promise<Array>} 树形或平铺的文件/文件夹结构数组
|
||
*/
|
||
async function readPaths(
|
||
path,
|
||
level = 0,
|
||
parentId = null,
|
||
fullPathNames = [],
|
||
isFileKey = ".json",
|
||
treeStructure = true
|
||
) {
|
||
const treeList = [];
|
||
|
||
// 获取当前路径下所有文件/文件夹
|
||
let pathSyncList = file.readPathSync(path);
|
||
|
||
if (!Array.isArray(pathSyncList)) {
|
||
pathSyncList = [...pathSyncList]
|
||
}
|
||
// log.error(JSON.stringify(...pathSyncList))
|
||
// log.error(JSON.stringify(Array.isArray(pathSyncList)))
|
||
//
|
||
// 可选:按名称排序,保证同层顺序可预测(字母序或自定义规则)
|
||
pathSyncList.sort((a, b) => a.localeCompare(b));
|
||
|
||
for (const itemPath of pathSyncList) {
|
||
const currentName = getFileOrFolderName(itemPath); // 请确保你有这个辅助函数,取路径最后一段
|
||
const currentId = itemPath; // 使用完整路径作为全局唯一 id(最可靠)
|
||
const currentFullPathNames = [...fullPathNames, currentName];
|
||
|
||
if (itemPath.endsWith(isFileKey)) {
|
||
// ── 是目标文件 ───────────────────────────────
|
||
let displayName;
|
||
let parentDisplayName;
|
||
|
||
// 你原有的复杂名称处理逻辑(可继续保留或简化)
|
||
const parentFolder = getParentFolderName(itemPath);
|
||
|
||
if (!parentFolder) {
|
||
throw new Error(`${itemPath} 没有上级目录`);
|
||
}
|
||
|
||
if (itemPath.includes("@")) {
|
||
displayName = getParentFolderName(itemPath, 2);
|
||
parentDisplayName = getParentFolderName(itemPath, 3);
|
||
} else {
|
||
displayName = parentFolder;
|
||
parentDisplayName = getParentFolderName(itemPath, 2);
|
||
}
|
||
|
||
const fileNode = {
|
||
id: currentId,
|
||
name: displayName || currentName,
|
||
parentName: parentDisplayName,
|
||
parentId: parentId,
|
||
path: itemPath,
|
||
level: level + 1,
|
||
fullPathNames: currentFullPathNames,
|
||
isRoot: false,
|
||
isFile: true,
|
||
children: [] // 文件没有子节点
|
||
};
|
||
|
||
treeList.push(fileNode);
|
||
} else if (file.IsFolder(itemPath)) {
|
||
// ── 是文件夹 ───────────────────────────────
|
||
const childNodes = await readPaths(
|
||
itemPath,
|
||
level + 1,
|
||
currentId, // 把当前路径作为子节点的 parentId
|
||
currentFullPathNames, // 传递路径栈
|
||
isFileKey,
|
||
treeStructure
|
||
);
|
||
|
||
if (treeStructure) {
|
||
// 树形结构:保留文件夹节点
|
||
const folderNode = {
|
||
id: currentId,
|
||
name: currentName,
|
||
parentName: fullPathNames.length > 0 ? fullPathNames[fullPathNames.length - 1] : undefined,
|
||
parentId: parentId,
|
||
path: itemPath,
|
||
level: level + 1,
|
||
fullPathNames: currentFullPathNames,
|
||
isRoot: level === 0,
|
||
isFile: false,
|
||
children: childNodes
|
||
};
|
||
treeList.push(folderNode);
|
||
} else {
|
||
// 非树形:只收集文件,文件夹本身不保留
|
||
treeList.push(...childNodes);
|
||
}
|
||
}
|
||
}
|
||
|
||
return treeList;
|
||
}
|
||
|
||
// 辅助函数示例(请根据你的实际实现替换)
|
||
function getFileOrFolderName(fullPath) {
|
||
// 返回路径最后一段名称
|
||
const parts = fullPath.split(/[\\/]/);
|
||
return parts[parts.length - 1];
|
||
}
|
||
|
||
function getParentFolderName(fullPath, upLevels = 1) {
|
||
// 返回上 N 级的文件夹名称
|
||
const parts = fullPath.split(/[\\/]/).filter(Boolean);
|
||
if (parts.length <= upLevels) return undefined;
|
||
|
||
const targetIndex = parts.length - 1 - upLevels;
|
||
return parts[targetIndex] || undefined;
|
||
}
|
||
|
||
async function treeToList(treeList = []) {
|
||
let list = []
|
||
for (const element of treeList) {
|
||
const child = element.children
|
||
if (child && child.length > 0) {
|
||
list = list.concat(await treeToList(child))
|
||
}
|
||
// 如果是文件,添加到结果列表
|
||
if (element.isFile) {
|
||
list.push(element);
|
||
}
|
||
}
|
||
return list
|
||
}
|
||
|