mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-04-03 06:35:14 +08:00
feat(auto-tools): 新增cron表达式解析功能并优化路径过滤逻辑
- 添加cron.js工具模块,实现cron表达式解析和下次执行时间计算功能 - 新增timeType枚举类型,支持小时和cron表达式两种时间配置方式 - 重构路径去重逻辑,使用Map按路径字符串和时间戳进行智能去重 - 移除废弃的getTimeDifference函数实现 - 在初始化配置中注册cron工具模块 - 实现基于时间配置的路径过滤机制,支持小时间隔和cron表达式触发 - 从pathing.json加载时间配置,按等级排序后应用到路径过滤逻辑 - 添加cron表达式解析失败的错误处理和日志记录 - 更新pathing.json默认配置,包含地方特产和矿物的时间规则示例
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
// 启用自动拾取的实时任务,并配置成启用急速拾取模式
|
||||
|
||||
@@ -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 * ?"
|
||||
}
|
||||
]
|
||||
|
||||
276
repo/js/FullyAutoAndSemiAutoTools/utils/cron.js
Normal file
276
repo/js/FullyAutoAndSemiAutoTools/utils/cron.js
Normal file
@@ -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<number>} 所有匹配的数值集合
|
||||
*/
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user