diff --git a/repo/js/FullyAutoAndSemiAutoTools/main.js b/repo/js/FullyAutoAndSemiAutoTools/main.js index 1a512986b..287676f55 100644 --- a/repo/js/FullyAutoAndSemiAutoTools/main.js +++ b/repo/js/FullyAutoAndSemiAutoTools/main.js @@ -67,6 +67,15 @@ const team = { fightKeys: ['锄地专区', "敌人与魔物"], SevenElements: settings.teamSevenElements ? settings.teamSevenElements.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); + } +}); + /** * 保存记录路径的函数 * 该函数将RecordPath对象中的Set类型数据转换为数组后保存到文件 @@ -77,17 +86,41 @@ async function saveRecordPaths() { const recordToSave = { // 使用展开运算符复制Record对象的所有属性,保持其他数据不变 ...RecordPath, - // 将paths Set转换为数组,以便能够序列化为JSON - paths: [...RecordPath.paths].map(item => ({ - // 保留name属性 - timestamp: item.timestamp, - // 将item中的paths Set转换为数组 - path: item.path - })) + // 处理 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(RecordText, JSON.stringify(recordToSave)) } + /** * 保存当前记录到记录列表并同步到文件 * 该函数在保存前会将Set类型的数据转换为数组格式,确保JSON序列化正常进行 @@ -163,51 +196,6 @@ function getTimeDifference(startTime, endTime) { } -/** - * 计算两个时间之间的差值,并返回指定格式的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 totalHours = totalSeconds / 3600; - const totalMinutes = totalSeconds / 60; - // 计算剩余小时、分钟、秒(即去掉完整天数后的部分) - const infoHours = totalHours % 24; - const infoMinutes = totalMinutes; - const infoSeconds = totalSecs; - - const diff_json = { - info: { - hours: infoHours, - minutes: infoMinutes, - seconds: infoSeconds - }, - total: { - hours: totalHours, - minutes: totalMinutes, - seconds: totalSecs - } - }; - - return diff_json; -} - -// 输出类似: -// { -// info: { hours: 整数, minutes: 小时后的整数, seconds: 分种后的整数 }, -// total: {全是小数 hours: 1.1, minutes: 算出总分钟, seconds: 算出总秒 } -// } - /** * 初始化记录函数 * 该函数用于初始化一条新的记录,包括设置UID、时间戳和调整后的日期数据 @@ -445,6 +433,7 @@ async function getValueByMultiCheckboxName(name) { async function init() { let settingsConfig = await initSettings(); let utils = [ + "cron", "SwitchTeam", "uid", ] @@ -658,6 +647,9 @@ async function init() { return map.keys().filter(key => key.startsWith(levelName)) })) } + const timeJson = new Set(JSON.parse(file.readTextSync(`${pathingName}.json`)).sort( + (a, b) => b.level - a.level + )) // 初始化needRunMap if (true) { // for (let key of pathAsMap.keys()) { @@ -669,6 +661,8 @@ async function init() { 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}\\`)) @@ -677,14 +671,60 @@ async function init() { 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} }); - if (list.length <= 0) { - continue + // 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); + if (!nextCronTimestamp) { + log.error(`cron表达式解析失败: {value}`, value) + throw new Error(`cron表达式解析失败: ${value}`) + } + 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 + }) } - needRunMap.set(settingsAsName.settings_as_name, { - paths: list, - as_name: settingsAsName.settings_as_name, - name: settingsAsName.settings_name - }) } } // 启用自动拾取的实时任务,并配置成启用急速拾取模式 diff --git a/repo/js/FullyAutoAndSemiAutoTools/pathing.json b/repo/js/FullyAutoAndSemiAutoTools/pathing.json index fe51488c7..dc40e8280 100644 --- a/repo/js/FullyAutoAndSemiAutoTools/pathing.json +++ b/repo/js/FullyAutoAndSemiAutoTools/pathing.json @@ -1 +1,20 @@ -[] +[ +{ + "name": "晶蝶", + "type": "hours", + "level": 2, + "value": 12 +}, +{ + "name": "地方特产", + "type": "hours", + "level": 1, + "value": 46 +}, +{ + "name": "矿物", + "type": "cron", + "level": 1, + "value": "0 0 0 1/3 * ?" +} +] diff --git a/repo/js/FullyAutoAndSemiAutoTools/utils/cron.js b/repo/js/FullyAutoAndSemiAutoTools/utils/cron.js new file mode 100644 index 000000000..00622864a --- /dev/null +++ b/repo/js/FullyAutoAndSemiAutoTools/utils/cron.js @@ -0,0 +1,276 @@ +// 分钟 小时 日期 月份 星期 [年份可选] +const cronRegex = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+(\S+))?$/; + +/** + * 解析单个 cron 字段,返回匹配的数值数组 + * @param {string} field - 字段值 如 "1,3-5,* /10" + * @param {number} min - 最小值 + * @param {number} max - 最大值 + * @param {number} [stepBase=0] - 步进基准(星期从0或1开始都支持) + * @returns {Set} 所有匹配的数值集合 + */ +function parseCronField(field, min, max, stepBase = 0) { + const result = new Set(); + const parts = field.split(','); + + for (const part of parts) { + // 处理 */n 格式 + if (part.startsWith('*/')) { + const step = parseInt(part.slice(2), 10); + if (isNaN(step) || step <= 0) continue; + + for (let i = min; i <= max; i += step) { + result.add(i); + } + continue; + } + + // 处理 n-n/n 格式 + if (part.includes('/')) { + const [range, stepStr] = part.split('/'); + const step = parseInt(stepStr, 10); + if (isNaN(step) || step <= 0) continue; + + if (range === '*') { + for (let i = min; i <= max; i += step) { + result.add(i); + } + } else if (range.includes('-')) { + const [start, end] = range.split('-').map(Number); + if (isNaN(start) || isNaN(end)) continue; + for (let i = Math.max(min, start); i <= Math.min(max, end); i += step) { + result.add(i); + } + } + continue; + } + + // 处理范围 n-n + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.max(min, start); i <= Math.min(max, end); i++) { + result.add(i); + } + } + continue; + } + + // 处理单个数字 / 列表 + const num = parseInt(part, 10); + if (!isNaN(num) && num >= min && num <= max) { + result.add(num); + } + + // 处理 L(最后一天) - 只对日期字段有意义,这里简单处理 + if (part === 'L' && max === 31) { + // 实际使用时需要知道当月天数,这里先占位 + result.add(99); // 特殊标记,后续需处理 + } + + // ? 在日期/星期中代表「无特定值」 - 这里简单忽略具体值 + if (part === '?') { + // 实际使用时需配合另一字段判断 + } + } + + return result; +} + +/** + * 简单版 cron 解析器(只解析出每个字段允许的时间值) + * @param {string} cron - cron表达式 "0 9 * * 1-5" + * @returns {object|null} 解析结果 或 null(格式错误) + */ +function parseCron(cron) { + const match = cron.trim().match(cronRegex); + if (!match) return null; + + const [, min, hour, day, month, dow] = match; + + try { + const minutes = parseCronField(min, 0, 59); + const hours = parseCronField(hour, 0, 23); + const days = parseCronField(day, 1, 31); + const months = parseCronField(month, 1, 12); + const dows = parseCronField(dow, 0, 7); // 0和7都代表周日 + + // 星期字段 7 → 0 + if (dows.has(7)) dows.add(0); + + return { + minutes: [...minutes].sort((a, b) => a - b), + hours: [...hours].sort((a, b) => a - b), + days: [...days].sort((a, b) => a - b), + months: [...months].sort((a, b) => a - b), + dows: [...dows].sort((a, b) => a - b), + // 注:days和dows同时有特定值时,实际cron是「或」关系 + // 这里仅简单分开列出,真实调度需复杂判断 + original: cron.trim(), + isValid: true + }; + } catch (e) { + return { isValid: false, error: e.message, original: cron }; + } +} + +/** + * 根据 cron 表达式和当前时间,计算下一次执行的时间戳(毫秒) + * 支持标准 5 段 cron: 分钟 时 日 月 星期 + * @param {string} cron - cron表达式,例如 "30 2 * * 1-5" + * @param {number} [fromTime=Date.now()] - 从这个时间开始找下一个执行点 + * @returns {number|null} 下一次执行的时间戳(毫秒),找不到返回 null + */ +function getNextCronTimestamp(cron, fromTime = Date.now(),endTime) { + const parts = cron.trim().split(/\s+/); + if (parts.length < 5 || parts.length > 6) { + throw new Error("不支持的 cron 格式,应为 5~6 段"); + } + + const [minStr, hourStr, dayStr, monthStr, dowStr] = parts; + + // 解析每个字段 + const minutes = parseField(minStr, 0, 59); + const hours = parseField(hourStr, 0, 23); + const days = parseField(dayStr, 1, 31); + const months = parseField(monthStr, 1, 12); + const dows = parseField(dowStr, 0, 7); + + // 星期 7 → 0 (周日) + if (dows.has(7)) dows.add(0); + + let current = new Date(fromTime); + // 如果没有指定 endTime,默认设置为明天 00:00:00 + if (endTime === undefined) { + const tomorrow = new Date(current); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + endTime = tomorrow.getTime(); + } + + // 动态计算最大迭代次数 + // 将时间差(毫秒)转换为分钟,向上取整,确保覆盖所有可能的分钟点 + const timeDiffMinutes = Math.ceil((endTime - fromTime) / 60000); + + // 设置最大迭代次数,防止意外情况(如 endTime 极大)导致内存溢出或死循环 + // 即使 endTime 是 10 年后,也限制在约 2 年内(避免极端情况) + // 2年 ≈ 365 * 2 * 24 * 60 = 1,051,200 + const MAX_ITERATIONS_LIMIT = 1051200; + const MAX_ITERATIONS = Math.min(timeDiffMinutes, MAX_ITERATIONS_LIMIT); + + let iteration = 0; + while (iteration++ < MAX_ITERATIONS) { + // 先推进到下一分钟,避免死循环在同一分钟 + current.setMinutes(current.getMinutes() + 1); + current.setSeconds(0); + current.setMilliseconds(0); + + const m = current.getMinutes(); + const h = current.getHours(); + const d = current.getDate(); + const mon = current.getMonth() + 1; // JS 月份 0~11 + const dow = current.getDay(); // 0=周日, 1=周一, ..., 6=周六 + + // 核心匹配条件(日期和星期是 OR 关系) + const minuteMatch = minutes.has(m) || minutes.size === 0; + const hourMatch = hours.has(h) || hours.size === 0; + const monthMatch = months.has(mon) || months.size === 0; + const dayMatch = days.has(d) || days.size === 0; + const dowMatch = dows.has(dow) || dows.size === 0; + + const dateOrDowMatch = (days.size === 0 && dows.size === 0) || // 两者都是 * + (days.size > 0 && dows.size === 0) || // 只指定了日期 + (days.size === 0 && dows.size > 0) || // 只指定了星期 + (dayMatch && dowMatch); // 两者都满足才算(最严格) + + if (minuteMatch && hourMatch && monthMatch && dateOrDowMatch) { + return current.getTime(); + } + } + + // 如果是因为超过 MAX_ITERATIONS_LIMIT 而退出,说明时间跨度太大 + if (timeDiffMinutes > MAX_ITERATIONS_LIMIT) { + log.warn("查找范围过大,已达到最大迭代次数限制"); + } else { + log.warn("未找到合理下一次执行时间"); + } + return null; +} + +/** + * 解析单个 cron 字段,返回匹配的数值 Set + * 支持: * , - / 数值列表 * /n + */ +function parseField(field, min, max) { + const result = new Set(); + + if (field === '*' || field === '?') { + return result; // 空 set 代表任意 + } + + const parts = field.split(','); + + for (let part of parts) { + part = part.trim(); + + // */n + if (part.startsWith('*/')) { + const step = Number(part.slice(2)); + if (!isNaN(step) && step > 0) { + for (let i = min; i <= max; i += step) { + result.add(i); + } + } + continue; + } + + // n-n + if (part.includes('-') && !part.includes('/')) { + const [start, end] = part.split('-').map(Number); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.max(min, start); i <= Math.min(max, end); i++) { + result.add(i); + } + } + continue; + } + + // n/n 或 */n 已处理,剩下是普通数字或范围/步进 + if (part.includes('/')) { + const [range, stepStr] = part.split('/'); + const step = Number(stepStr); + if (isNaN(step) || step <= 0) continue; + + if (range === '*') { + for (let i = min; i <= max; i += step) result.add(i); + } else { + const [start, end] = range.split('-').map(Number); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.max(min, start); i <= Math.min(max, end); i += step) { + result.add(i); + } + } + } + continue; + } + + // 单个数字 + const num = Number(part); + if (!isNaN(num) && num >= min && num <= max) { + result.add(num); + } + } + + return result; +} + +function a(){ + return true +} + +this.cronUtil = { + getNextCronTimestamp, + parseCron, + parseField, + parseCronField, +} \ No newline at end of file