js: CD-Aware-AutoGather: 队伍中没有对应元素角色时自动切换采集队伍 (#1166)
* js: CD-Aware-AutoGather: 队伍中没有对应元素角色时自动切换采集队伍 其他细节优化: - 修复路径中有空格时匹配不到刷新机制的问题(`1. 高成功率路线`) - 扫描材料时统计角色需求 - 将辅助功能抽取为库,`main.js`只保留核心逻辑 * js: CD-Aware-AutoGather: 增加全选选项,便于直接采用全部路径订阅的路线
This commit is contained in:
63
repo/js/CD-Aware-AutoGather/CooldownData.txt
Normal file
63
repo/js/CD-Aware-AutoGather/CooldownData.txt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
沉玉仙茗: 24小时
|
||||||
|
发光髓: 12小时
|
||||||
|
蝴蝶翅膀: 12小时
|
||||||
|
晶核: 12小时
|
||||||
|
鳗肉: 12小时
|
||||||
|
螃蟹: 12小时
|
||||||
|
禽肉: 12小时
|
||||||
|
青蛙: 12小时
|
||||||
|
鳅鳅宝玉: 12小时
|
||||||
|
神秘的肉: 12小时
|
||||||
|
兽肉: 12小时
|
||||||
|
蜥蜴尾巴: 12小时
|
||||||
|
鱼肉: 12小时
|
||||||
|
白萝卜: 每天0点
|
||||||
|
薄荷: 每天0点
|
||||||
|
澄晶实: 每天0点
|
||||||
|
墩墩桃: 每天0点
|
||||||
|
海草: 每天0点
|
||||||
|
红果果菇: 每天0点
|
||||||
|
胡萝卜: 每天0点
|
||||||
|
金鱼草: 每天0点
|
||||||
|
堇瓜: 每天0点
|
||||||
|
烬芯花: 每天0点
|
||||||
|
久雨莲: 每天0点
|
||||||
|
颗粒果: 每天0点
|
||||||
|
苦种: 每天0点
|
||||||
|
莲蓬: 每天0点
|
||||||
|
烈焰花花蕊: 每天0点
|
||||||
|
马尾: 每天0点
|
||||||
|
蘑菇: 每天0点
|
||||||
|
茉洁草: 每天0点
|
||||||
|
鸟蛋: 每天0点
|
||||||
|
泡泡桔: 每天0点
|
||||||
|
苹果: 每天0点
|
||||||
|
日落果: 每天0点
|
||||||
|
树莓: 每天0点
|
||||||
|
松果: 每天0点
|
||||||
|
松茸: 每天0点
|
||||||
|
甜甜花: 每天0点
|
||||||
|
汐藻: 每天0点
|
||||||
|
香辛果: 每天0点
|
||||||
|
星蕈: 每天0点
|
||||||
|
须弥蔷薇: 每天0点
|
||||||
|
枣椰: 每天0点
|
||||||
|
竹笋: 每天0点
|
||||||
|
烛伞蘑菇: 每天0点
|
||||||
|
沉玉仙茗: 24小时
|
||||||
|
晶蝶: 每天4点
|
||||||
|
|
||||||
|
铁块: 每天0点
|
||||||
|
白铁块: 每2天0点
|
||||||
|
电气水晶: 每2天0点
|
||||||
|
星银矿石: 每2天0点
|
||||||
|
萃凝晶: 每3天0点
|
||||||
|
水晶块: 每3天0点
|
||||||
|
紫晶块: 每3天0点
|
||||||
|
奇异的「牙齿」: 46小时
|
||||||
|
冰雾花花朵: 46小时
|
||||||
|
冰雾花: 46小时
|
||||||
|
烈焰花花蕊: 46小时
|
||||||
|
烈焰花: 46小时
|
||||||
|
|
||||||
|
地方特产: 46小时
|
||||||
@@ -28,18 +28,19 @@
|
|||||||
|
|
||||||
| 选项 | 说明 |
|
| 选项 | 说明 |
|
||||||
| ---- | ---- |
|
| ---- | ---- |
|
||||||
| 设置要使用的队伍名称 | 执行采集任务前切换到指定的队伍,未设置则不切换。 |
|
| 设置首选队伍名称 | 执行采集任务前切换到指定的队伍,未设置则不切换。 |
|
||||||
|
| 设置备选队伍名称 | 首选队伍缺少对应的采集角色时使用。<br>两支队伍的名称不要存在包含关系,例如不能一支叫`特产`一支叫`特产备选` |
|
||||||
| 停止运行时间 | 超过此时间后,停止后续的任务(会等待正在运行的那条json路线结束)。 |
|
| 停止运行时间 | 超过此时间后,停止后续的任务(会等待正在运行的那条json路线结束)。 |
|
||||||
| 我肝的账号不止一个 | 如果你有多个账号,可以选中此选项,选中后将分账号维护对应的材料刷新时间。 |
|
| 我肝的账号不止一个 | 如果你有多个账号,可以选中此选项,选中后将分账号维护对应的材料刷新时间。 |
|
||||||
|
| 采集扫描到的所有材料 | 选中后将不管后面的每个材料⬇️的选项实际是否勾选,全都视为已勾选 |
|
||||||
|
| 即使同一种材料有多个版本的路线,也全都执行采集 | 如果某种材料选中了多个版本的路线(常见于不同作者),默认只会执行第一个。勾选此选项后会每个版本都执行,可能造成部分点位重复(空跑)。 |
|
||||||
| `↓` 地方特产\稻妻\绯樱绣球 | 根据你订阅的路径追踪任务数量,这里将会显示相应个数的选择框。<br>勾选后将执行你选中的条目的采集任务。<br>Tip: `↓`符号是在提示你应该勾选文本下面的选择框 |
|
| `↓` 地方特产\稻妻\绯樱绣球 | 根据你订阅的路径追踪任务数量,这里将会显示相应个数的选择框。<br>勾选后将执行你选中的条目的采集任务。<br>Tip: `↓`符号是在提示你应该勾选文本下面的选择框 |
|
||||||
|
|
||||||
运行此模式后,将按照你勾选的条目,执行相应的采集任务。每执行完一条json路线后,将会计算它的下次刷新时间并写入`record`文件夹下的记录文件。下次运行脚本时,未刷新的路线将自动跳过。
|
运行此模式后,将按照你勾选的条目,执行相应的采集任务。每执行完一条json路线后,将会计算它的下次刷新时间并写入`record`文件夹下的记录文件。下次运行脚本时,未刷新的路线将自动跳过。
|
||||||
|
|
||||||
可以同时勾选多种材料,会逐个进行采集。
|
可以同时勾选多种材料,会逐个进行采集。
|
||||||
|
|
||||||
如果不同的采集任务需要不同队伍,那请在调度器配置组里添加多次本脚本,然后分别设置不同的采集物和采集队伍。
|
采集任务可能用到的元素共有`火水雷风`4种,此外还有挖矿类(如钟离)以及纳西妲两个类型,可以考虑建立两支队伍`钟纳水雷`和`钟纳火风`,即可满足所有采集任务的需要。
|
||||||
|
|
||||||
> 采集任务可能用到的元素共有`火水雷风`4种,此外还有挖矿类(如钟离)以及纳西妲两个类型,可以考虑建立两支队伍`钟纳火水`和`钟纳雷风`,即可满足所有采集任务的需要。
|
|
||||||
|
|
||||||
支持使用配置组`更多功能`——`日志分析`分析运行记录(参考了[mno](https://github.com/Bedrockx)大佬的写法)。
|
支持使用配置组`更多功能`——`日志分析`分析运行记录(参考了[mno](https://github.com/Bedrockx)大佬的写法)。
|
||||||
|
|
||||||
|
|||||||
596
repo/js/CD-Aware-AutoGather/lib/lib.js
Normal file
596
repo/js/CD-Aware-AutoGather/lib/lib.js
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
/**
|
||||||
|
* @author Ayaka-Main
|
||||||
|
* @link https://github.com/Patrick-Ze
|
||||||
|
* @description 提供一些通用性的功能函数。使用方法: 将此文件放在脚本目录下的 lib 文件夹中,然后在你的脚本开头处执行下面这行:
|
||||||
|
eval(file.readTextSync("lib/lib.js"));
|
||||||
|
*/
|
||||||
|
|
||||||
|
let scriptContext = {
|
||||||
|
scriptStartTime: new Date(),
|
||||||
|
version: "1.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Date 对象格式化为 ISO 8601 字符串,包含本地时区(如:2020-09-28T20:20:20.999+08:00)
|
||||||
|
* @param {Date} date - 要格式化的日期对象
|
||||||
|
* @returns {string} 格式化后的字符串
|
||||||
|
*/
|
||||||
|
function formatDateTime(date) {
|
||||||
|
const pad = (n) => n.toString().padStart(2, "0");
|
||||||
|
const padMs = (n) => n.toString().padStart(3, "0");
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = pad(date.getMonth() + 1);
|
||||||
|
const day = pad(date.getDate());
|
||||||
|
const hour = pad(date.getHours());
|
||||||
|
const minute = pad(date.getMinutes());
|
||||||
|
const second = pad(date.getSeconds());
|
||||||
|
const ms = padMs(date.getMilliseconds());
|
||||||
|
|
||||||
|
// 获取时区偏移(分钟),转换成±HH:MM
|
||||||
|
const offset = -date.getTimezoneOffset();
|
||||||
|
const sign = offset >= 0 ? "+" : "-";
|
||||||
|
const offsetHour = pad(Math.floor(Math.abs(offset) / 60));
|
||||||
|
const offsetMin = pad(Math.abs(offset) % 60);
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}${sign}${offsetHour}:${offsetMin}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Date 对象以本地时区格式化为字符串,格式为 "MM-DD HH:mm:ss"
|
||||||
|
* @param {Date} date - 要格式化的日期对象
|
||||||
|
* @returns {string} 格式化后的字符串
|
||||||
|
*/
|
||||||
|
function formatDateTimeShort(date) {
|
||||||
|
const pad = (n) => n.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
const month = pad(date.getMonth() + 1);
|
||||||
|
const day = pad(date.getDate());
|
||||||
|
const hour = pad(date.getHours());
|
||||||
|
const minute = pad(date.getMinutes());
|
||||||
|
const second = pad(date.getSeconds());
|
||||||
|
|
||||||
|
return `${month}-${day} ${hour}:${minute}:${second}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前时间是否已达到目标时间(目标时间基于脚本启动时间,支持跨天)。
|
||||||
|
* @param {string} targetTimeStr - 目标时间,格式为 "HH:mm"。
|
||||||
|
* @returns {boolean} 如果已达到目标时间,返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
function isTargetTimeReached(targetTimeStr) {
|
||||||
|
const now = new Date();
|
||||||
|
const [targetHour, targetMinute] = targetTimeStr.split(":").map(Number);
|
||||||
|
|
||||||
|
const target = new Date(scriptContext.scriptStartTime);
|
||||||
|
target.setHours(targetHour, targetMinute, 0, 0);
|
||||||
|
|
||||||
|
// 如果目标时间早于脚本启动时间,则认为是第二天
|
||||||
|
if (target <= scriptContext.scriptStartTime) {
|
||||||
|
target.setDate(target.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return now >= target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前时间是否在给定时间范围内(支持跨天)。
|
||||||
|
* @param {*} startStr 起始时间,格式为"HH:mm"
|
||||||
|
* @param {*} endStr 结束时间,格式为"HH:mm"
|
||||||
|
* @returns {boolean} 如果当前时间在范围内,返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
function isNowInTimeRange(startStr, endStr) {
|
||||||
|
const now = new Date();
|
||||||
|
const [startHour, startMinute] = startStr.split(":").map(Number);
|
||||||
|
const [endHour, endMinute] = endStr.split(":").map(Number);
|
||||||
|
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setHours(startHour, startMinute, 0, 0);
|
||||||
|
|
||||||
|
const end = new Date(now);
|
||||||
|
end.setHours(endHour, endMinute, 0, 0);
|
||||||
|
|
||||||
|
// 如果结束时间早于开始时间,表示跨天
|
||||||
|
if (end <= start) {
|
||||||
|
end.setDate(end.getDate() + 1);
|
||||||
|
}
|
||||||
|
return now >= start && now <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据上期刷新时间字符串和刷新模式计算下一次的刷新时间。
|
||||||
|
*
|
||||||
|
* @param {string} lastRefreshTimeStr 上次刷新时间。如果为空或无效,将使用 getDefaultTime()。
|
||||||
|
* @param {string} refreshMode 刷新模式,例如 "每X周", "每X天Y点", "每24:05" (表示每24小时零5分), "X小时"
|
||||||
|
* @returns {Date | null} 计算出的下一次刷新时间Date对象,如果模式无法解析则返回null。
|
||||||
|
* @example 已进行过的测试用例(用例中 GetDefaultTime() 返回 1970-01-01T00:00:00.000+08:00):
|
||||||
|
* calculateNextRefreshTime("2025-06-01T10:00:00.000+08:00", "每1周"); // 2025-06-02T04:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-02T03:00:00.000+08:00", "每1周"); // 2025-06-02T04:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-02T05:00:00.000+08:00", "每1周"); // 2025-06-09T04:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime(null, "每周"); // 1970-01-05T04:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-02T03:00:00.000+08:00", "每2周"); // 2025-06-09T04:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T22:00:00.000+08:00", "每天8点"); // 2025-06-21T08:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T07:00:00.000+08:00", "每天08点"); // 2025-06-21T08:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T09:00:00.000+08:00", "每天08点"); // 2025-06-22T08:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime(null, "每天12点"); // 1970-01-01T12:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T10:00:00.000+08:00", "每2天10点"); // 2025-06-22T10:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T10:00:00.000+08:00", "每3天0点"); // 2025-06-23T00:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T11:00:00.000+08:00", "00:30"); // 2025-06-21T11:30:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T23:00:00.000+08:00", "02:00"); // 2025-06-22T01:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T04:00:00.000+08:00", "每24:05"); // 2025-06-21T04:05:00.000+08:00
|
||||||
|
* calculateNextRefreshTime(null, "01:00"); // 1970-01-01T01:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T10:00:00.000+08:00", "2小时"); // 2025-06-21T12:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T23:00:00.000+08:00", "3小时"); // 2025-06-22T02:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime(null, "5小时"); // 1970-01-01T05:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T10:00:00.000+08:00", "每1周 每天10点"); // 2025-06-23T04:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T10:00:00.000+08:00", "每天10点 02:00 2小时"); // 2025-06-21T10:00:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-20T10:00:00.000+08:00", "00:30 2小时"); // 2025-06-20T10:30:00.000+08:00
|
||||||
|
* calculateNextRefreshTime("2025-06-21T10:00:00.000+08:00", "无效模式"); // null
|
||||||
|
*/
|
||||||
|
function calculateNextRefreshTime(lastRefreshTimeStr, refreshMode) {
|
||||||
|
let lastRunTime = lastRefreshTimeStr ? new Date(lastRefreshTimeStr) : getDefaultTime();
|
||||||
|
let nextRunTime = null;
|
||||||
|
const lowerCaseRefreshMode = refreshMode.toLowerCase();
|
||||||
|
|
||||||
|
// 1. 匹配 "每(\d*)周"
|
||||||
|
let match = lowerCaseRefreshMode.match(/每(\d*)周/);
|
||||||
|
if (match) {
|
||||||
|
const weeks = parseInt(match[1] || "1", 10); // 如果没有数字,默认为1周
|
||||||
|
|
||||||
|
nextRunTime = new Date(lastRunTime);
|
||||||
|
// 找到 lastRunTime 所在周的周一 04:00
|
||||||
|
nextRunTime.setDate(lastRunTime.getDate() - ((lastRunTime.getDay() + 6) % 7)); // 调整到上一个或当前周一
|
||||||
|
nextRunTime.setHours(4, 0, 0, 0); // 固定到周一 04:00
|
||||||
|
|
||||||
|
// 确保 nextRunTime 至少晚于 lastRunTime。
|
||||||
|
// 如果 lastRunTime 是周一 05:00,而计算出的是周一 04:00,则需要推到下个周期。
|
||||||
|
while (nextRunTime <= lastRunTime) {
|
||||||
|
nextRunTime.setDate(nextRunTime.getDate() + 7);
|
||||||
|
}
|
||||||
|
if (weeks > 1) {
|
||||||
|
// 如果是多周周期,直接加上 weeks 周
|
||||||
|
nextRunTime.setDate(nextRunTime.getDate() + 7 * (weeks - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 匹配 "每(\d*)天(\d{1,2})点"
|
||||||
|
if (!nextRunTime) {
|
||||||
|
match = lowerCaseRefreshMode.match(/每(\d*)天(\d{1,2})点/);
|
||||||
|
if (match) {
|
||||||
|
const days = parseInt(match[1] || "1", 10); // 如果没有数字,默认为1天
|
||||||
|
const hours = parseInt(match[2], 10);
|
||||||
|
|
||||||
|
nextRunTime = new Date(lastRunTime);
|
||||||
|
nextRunTime.setHours(hours, 0, 0, 0); // 设置固定小时和分钟
|
||||||
|
|
||||||
|
// 确保 nextRunTime 至少晚于 lastRunTime。
|
||||||
|
while (nextRunTime <= lastRunTime) {
|
||||||
|
nextRunTime.setDate(nextRunTime.getDate() + days);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 匹配 "每(\d\d):(\d\d)" (作为间隔)
|
||||||
|
if (!nextRunTime) {
|
||||||
|
match = lowerCaseRefreshMode.match(/(\d{1,2}):(\d{2})/);
|
||||||
|
if (match) {
|
||||||
|
const intervalHours = parseInt(match[1], 10);
|
||||||
|
const intervalMinutes = parseInt(match[2], 10);
|
||||||
|
const intervalMs = (intervalHours * 60 + intervalMinutes) * 60 * 1000;
|
||||||
|
|
||||||
|
if (intervalMs > 0) {
|
||||||
|
// 确保间隔有效
|
||||||
|
nextRunTime = new Date(lastRunTime.getTime() + intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 匹配 "(\d+)小时"
|
||||||
|
if (!nextRunTime) {
|
||||||
|
match = lowerCaseRefreshMode.match(/(\d+)小时/);
|
||||||
|
if (match) {
|
||||||
|
const intervalHours = parseInt(match[1], 10);
|
||||||
|
const intervalMs = intervalHours * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
if (intervalMs > 0) {
|
||||||
|
// 确保间隔有效
|
||||||
|
nextRunTime = new Date(lastRunTime.getTime() + intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRunTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断任务是否达到刷新时间
|
||||||
|
*
|
||||||
|
* @param {string} refreshMode 刷新模式,例如 "每X周", "每X天Y点", "X小时", "每24:05" (表示每24小时零5分)
|
||||||
|
* @param {string} taskName 任务名称或采集资源名称
|
||||||
|
* @param {string} [accountName] 账户名称,可选
|
||||||
|
* @returns {{isRefreshed: boolean, lastRunTime: Date | null, nextRunTime: Date | null}}
|
||||||
|
* 返回一个对象,包含:
|
||||||
|
* - isRefreshed: boolean - 任务是否达到刷新时间。
|
||||||
|
* - lastRunTime: Date | null - 任务上次运行的时间(如果未找到,则是getDefaultTime()返回的远古时间)。
|
||||||
|
* - nextRunTime: Date | null - 计算出的下一次刷新时间。
|
||||||
|
*/
|
||||||
|
function isTaskRefreshed(refreshMode, taskName, accountName = null) {
|
||||||
|
let record = {};
|
||||||
|
const recordPath = `record/${accountName || "默认账号"}.json`;
|
||||||
|
try {
|
||||||
|
const content = file.readTextSync(recordPath);
|
||||||
|
record = JSON.parse(content);
|
||||||
|
} catch (e) {
|
||||||
|
log.debug(`无法读取或解析记录文件 ${recordPath},错误: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
taskName = taskName || "默认任务";
|
||||||
|
const lastRunTimeStr = record[taskName];
|
||||||
|
const currentTime = new Date();
|
||||||
|
const nextRunTime = calculateNextRefreshTime(lastRunTimeStr, refreshMode);
|
||||||
|
|
||||||
|
let isRefreshed = false;
|
||||||
|
if (!nextRunTime) {
|
||||||
|
log.error(`无法解析刷新模式 "{0}",请检查格式`, refreshMode);
|
||||||
|
} else {
|
||||||
|
isRefreshed = currentTime >= nextRunTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastRunTime = lastRunTimeStr ? new Date(lastRunTimeStr) : getDefaultTime();
|
||||||
|
return {
|
||||||
|
isRefreshed: isRefreshed,
|
||||||
|
lastRunTime: lastRunTime, // 返回实际的 Date 对象
|
||||||
|
nextRunTime: nextRunTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断任务或资源是否仍然未刷新(对`isTaskRefreshed`的易用封装)
|
||||||
|
*
|
||||||
|
* @param {string} refreshMode 刷新模式,例如 "每X周", "每X天Y点", "X小时", "每24:05" (表示每24小时零5分)
|
||||||
|
* @param {string} taskName 任务名称或采集资源名称,可选
|
||||||
|
* @param {string} [accountName] 账户名称,可选
|
||||||
|
* @example
|
||||||
|
* // 运行结束时调用
|
||||||
|
* updateTaskRunTime();
|
||||||
|
* // 在脚本开头检查是否已刷新
|
||||||
|
* if (taskIsNotRefresh("每天4点")) {
|
||||||
|
* return;
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
function taskIsNotRefresh(refreshMode, taskName = null, accountName = null) {
|
||||||
|
const { isRefreshed, lastRunTime, nextRunTime } = isTaskRefreshed(refreshMode, taskName, accountName);
|
||||||
|
|
||||||
|
taskName = taskName || "默认任务";
|
||||||
|
if (!isRefreshed) {
|
||||||
|
log.info("{0}未刷新(上次运行: {1}), 刷新时间: {2}", taskName, formatDateTimeShort(lastRunTime), formatDateTimeShort(nextRunTime));
|
||||||
|
}
|
||||||
|
return !isRefreshed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定任务的上次运行时间为当前时间。
|
||||||
|
*
|
||||||
|
* @param {string} taskName 任务名称。
|
||||||
|
* @param {string} [accountName=null] 账户名称,可选,默认为null,表示使用默认账户。
|
||||||
|
* @returns {boolean} 如果成功更新了任务的上次运行时间则返回true,否则返回false。
|
||||||
|
*/
|
||||||
|
function updateTaskRunTime(taskName = null, accountName = null) {
|
||||||
|
let record = {};
|
||||||
|
taskName = taskName || "默认任务";
|
||||||
|
const recordPath = `record/${accountName || "默认账号"}.json`;
|
||||||
|
|
||||||
|
// 1. 读取记录文件
|
||||||
|
try {
|
||||||
|
const content = file.readTextSync(recordPath);
|
||||||
|
record = JSON.parse(content);
|
||||||
|
} catch (e) {
|
||||||
|
log.debug(`未能读取或解析记录文件 ${recordPath},将创建新记录。错误: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新指定任务的上次运行时间
|
||||||
|
const currentTime = new Date();
|
||||||
|
record[taskName] = formatDateTime(currentTime); // 格式化为本地时间字符串,便于人阅读
|
||||||
|
|
||||||
|
// 3. 将更新后的记录写回文件
|
||||||
|
try {
|
||||||
|
file.writeTextSync(recordPath, JSON.stringify(record, null, 2));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`写入文件 ${recordPath} 失败: ${e.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试切换队伍,如果失败则传送到七天神像后重试。
|
||||||
|
* @param {string} partyName - 要切换的队伍名
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function switchPartySafely(partyName) {
|
||||||
|
if (!partyName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await genshin.switchParty(partyName))) {
|
||||||
|
log.info("切换队伍失败,前往七天神像重试");
|
||||||
|
await genshin.tpToStatueOfTheSeven();
|
||||||
|
await genshin.returnMainUi(); // 确保传送完成
|
||||||
|
await genshin.switchParty(partyName);
|
||||||
|
await genshin.returnMainUi();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log.error("队伍切换失败,可能处于联机模式或其他不可切换状态");
|
||||||
|
await genshin.returnMainUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取账号名(通常用于区分不同账号的数据)
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {*} multiAccount 是否使用OCR区分多个账号(可以传入一个设置项)
|
||||||
|
* @returns {Promise<string>} 当前账号的UID,如果不区分多账号或OCR失败则返回"默认账号"。
|
||||||
|
*/
|
||||||
|
async function getGameAccount(multiAccount = false) {
|
||||||
|
let account = "默认账号";
|
||||||
|
if (!multiAccount) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开背包避免界面背景干扰
|
||||||
|
// await genshin.returnMainUi();
|
||||||
|
// keyPress("B");
|
||||||
|
// await sleep(1000);
|
||||||
|
|
||||||
|
const region = captureGameRegion();
|
||||||
|
const ocrResults = RecognitionObject.ocr(region.width * 0.75, region.height * 0.75, region.width * 0.25, region.height * 0.25);
|
||||||
|
const resList = region.findMulti(ocrResults);
|
||||||
|
|
||||||
|
for (let i = 0; i < resList.count; i++) {
|
||||||
|
const text = resList[i].text;
|
||||||
|
if (text.includes("UID")) {
|
||||||
|
const match = text.match(/\d+/);
|
||||||
|
if (match) {
|
||||||
|
account = match[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account === "默认账号") {
|
||||||
|
log.error("未能提取到UID");
|
||||||
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取脚本所在文件夹路径
|
||||||
|
* @returns {string|null} 脚本所在文件夹路径,若未获取到则返回 null
|
||||||
|
*/
|
||||||
|
function getScriptDirPath() {
|
||||||
|
try {
|
||||||
|
file.readTextSync(`Ayaka-Main-${Math.random()}.txt`);
|
||||||
|
} catch (error) {
|
||||||
|
const err_msg = error.toString();
|
||||||
|
const match = err_msg.match(/'([^']+)'/);
|
||||||
|
const fullPath = match ? match[1] : null;
|
||||||
|
const folderPath = fullPath ? fullPath.replace(/\\[^\\]+$/, "") : null;
|
||||||
|
return folderPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 manifest.json 获取脚本自身名称
|
||||||
|
* @returns {string} 脚本名称
|
||||||
|
*/
|
||||||
|
function getScriptName() {
|
||||||
|
const content = file.readTextSync("manifest.json");
|
||||||
|
const manifest = JSON.parse(content);
|
||||||
|
return manifest.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件路径中提取文件名。
|
||||||
|
* @param {string} filePath - 文件路径。
|
||||||
|
* @returns {string} - 文件名。
|
||||||
|
*/
|
||||||
|
function basename(filePath) {
|
||||||
|
const lastSlashIndex = filePath.lastIndexOf('\\'); // 或者使用 '/'
|
||||||
|
return filePath.substring(lastSlashIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将路径分割为目录和文件名
|
||||||
|
* @param {string} path - 文件完整路径
|
||||||
|
* @returns {[string, string]} 返回数组,第一个元素是目录路径,第二个是文件名
|
||||||
|
* @example
|
||||||
|
* const [dir, file] = splitPath('稻妻\\绯樱绣球\\06-绯樱绣球-神里屋敷-10个.json'); // ['稻妻\\绯樱绣球', '06-绯樱绣球-神里屋敷-10个.json']
|
||||||
|
*/
|
||||||
|
function splitPath(path) {
|
||||||
|
const normalizedPath = path.replace(/\\/g, "/");
|
||||||
|
const lastSlashIndex = normalizedPath.lastIndexOf("/");
|
||||||
|
if (lastSlashIndex === -1) {
|
||||||
|
return ["", path];
|
||||||
|
}
|
||||||
|
const dir = path.slice(0, lastSlashIndex);
|
||||||
|
const file = path.slice(lastSlashIndex + 1);
|
||||||
|
return [dir, file];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将路径分割为主名和扩展名
|
||||||
|
* @param {string} filename - 文件名或路径中的文件部分
|
||||||
|
* @returns {[string, string]} 返回数组,第一个是主文件名,第二个是扩展名(含点)
|
||||||
|
* @example
|
||||||
|
* const [dir, file] = splitPath('稻妻\\绯樱绣球\\06-绯樱绣球-神里屋敷-10个.json'); // ['稻妻\\绯樱绣球\\06-绯樱绣球-神里屋敷-10个', '.json']
|
||||||
|
*/
|
||||||
|
function splitExt(filename) {
|
||||||
|
const baseName = filename.includes("/") ? filename.slice(filename.lastIndexOf("/") + 1) : filename;
|
||||||
|
const lastDotIndex = baseName.lastIndexOf(".");
|
||||||
|
if (lastDotIndex <= 0) {
|
||||||
|
return [filename, ""];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
filename.slice(0, filename.length - (baseName.length - lastDotIndex)),
|
||||||
|
filename.slice(filename.length - (baseName.length - lastDotIndex)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你需要一个很久以前的时间,作为默认时间
|
||||||
|
* @returns {Date} 默认时间的Date对象
|
||||||
|
*/
|
||||||
|
function getDefaultTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear() - 18;
|
||||||
|
return new Date(year, 8, 28, 0, 0, 0); // 9月是month=8(0起始)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定目录下所有指定后缀的文件列表(不含子文件夹)
|
||||||
|
* @param {string} taskDir - 目标目录路径
|
||||||
|
* @param {string} [ext=".json"] - 文件后缀名(默认.json)
|
||||||
|
* @returns {string[]} 返回符合后缀的文件路径数组
|
||||||
|
*/
|
||||||
|
function getFilesByExtension(taskDir, ext = ".json") {
|
||||||
|
const allFilesRaw = file.ReadPathSync(taskDir);
|
||||||
|
const extFiles = [];
|
||||||
|
|
||||||
|
for (const filePath of allFilesRaw) {
|
||||||
|
if (filePath.endsWith(ext)) {
|
||||||
|
extFiles.push(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定路径下所有最底层的文件夹(即不包含任何子文件夹的文件夹)
|
||||||
|
* @param {string} folderPath - 要遍历的根文件夹路径
|
||||||
|
* @param {string[]} result - 用于收集最底层文件夹路径的数组
|
||||||
|
* @returns {Promise<string[]>} 所有最底层文件夹的路径
|
||||||
|
*/
|
||||||
|
function getLeafFolders(folderPath, result = []) {
|
||||||
|
const filesInSubFolder = file.ReadPathSync(folderPath);
|
||||||
|
let hasSubFolder = false;
|
||||||
|
|
||||||
|
for (const filePath of filesInSubFolder) {
|
||||||
|
if (file.IsFolder(filePath)) {
|
||||||
|
hasSubFolder = true;
|
||||||
|
// 递归查找子文件夹
|
||||||
|
getLeafFolders(filePath, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有发现任何子文件夹,则当前为最底层文件夹
|
||||||
|
if (!hasSubFolder) {
|
||||||
|
result.push(folderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考了 mno 大佬的函数
|
||||||
|
function _fakeLogCore(name, isJs = true, dateIn = null) {
|
||||||
|
const isStart = isJs === (dateIn !== null);
|
||||||
|
const lastRun = isJs ? new Date() : dateIn;
|
||||||
|
const task = isJs ? "JS脚本" : "地图追踪任务";
|
||||||
|
let logMessage = "";
|
||||||
|
let logTime = new Date();
|
||||||
|
if (isJs && isStart) {
|
||||||
|
logTime = dateIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间部分从第11位开始,长度是12("20:20:20.999")
|
||||||
|
const formattedTime = formatDateTime(logTime).slice(11, 23);
|
||||||
|
|
||||||
|
if (isStart) {
|
||||||
|
logMessage =
|
||||||
|
`正在伪造开始的日志记录\n\n` +
|
||||||
|
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
||||||
|
`------------------------------\n\n` +
|
||||||
|
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
||||||
|
`→ 开始执行${task}: "${name}"`;
|
||||||
|
} else {
|
||||||
|
const durationInSeconds = (logTime.getTime() - lastRun.getTime()) / 1000;
|
||||||
|
const durationMinutes = Math.floor(durationInSeconds / 60);
|
||||||
|
const durationSeconds = (durationInSeconds % 60).toFixed(3); // 保留三位小数
|
||||||
|
logMessage =
|
||||||
|
`正在伪造结束的日志记录\n\n` +
|
||||||
|
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
||||||
|
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}分${durationSeconds}秒\n\n` +
|
||||||
|
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
||||||
|
`------------------------------`;
|
||||||
|
}
|
||||||
|
log.debug(logMessage);
|
||||||
|
return logTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在日志文件中创建可供BGI解析耗时的路径追踪记录,Start和End两个函数需配对使用
|
||||||
|
* @param {string} name 要写入到日志的事项名,例如路径追踪的json文件名
|
||||||
|
* @returns {Date} 此函数的调用时间的Date对象
|
||||||
|
* @example
|
||||||
|
* let pathStart = logFakePathStart(fileName);
|
||||||
|
* // await pathingScript.runFile(jsonPath);
|
||||||
|
* logFakePathEnd(fileName, pathStart);
|
||||||
|
*/
|
||||||
|
function logFakePathStart(name) {
|
||||||
|
return _fakeLogCore(name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在日志文件中创建可供BGI解析耗时的路径追踪记录,Start和End两个函数需配对使用
|
||||||
|
* @param {string} name 要写入到日志的事项名,通常传入路径追踪的json文件名
|
||||||
|
* @param {Date} startTime 调用`logFakePathStart`时返回的Date对象
|
||||||
|
* @example
|
||||||
|
* let pathStart = logFakePathStart(fileName);
|
||||||
|
* // await pathingScript.runFile(jsonPath);
|
||||||
|
* logFakePathEnd(fileName, pathStart);
|
||||||
|
*/
|
||||||
|
function logFakePathEnd(name, startTime) {
|
||||||
|
return _fakeLogCore(name, false, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在日志文件中创建可供BGI解析耗时的脚本运行记录,Start和End两个函数需配对使用
|
||||||
|
* @param {string} scriptName 脚本名,留空时将自动获取
|
||||||
|
* @returns {Date} 此函数的调用时间的Date对象
|
||||||
|
* @example
|
||||||
|
* let startTime = logFakeScriptStart();
|
||||||
|
* // do something;
|
||||||
|
* logFakeScriptEnd({ startTime: startTime });
|
||||||
|
*/
|
||||||
|
function logFakeScriptStart(scriptName = null) {
|
||||||
|
if (!scriptName) {
|
||||||
|
if (!scriptContext.scriptName) {
|
||||||
|
scriptContext.scriptName = getScriptName();
|
||||||
|
}
|
||||||
|
scriptName = scriptContext.scriptName;
|
||||||
|
}
|
||||||
|
return _fakeLogCore(scriptName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在日志文件中创建可供BGI解析耗时的脚本运行记录,Start和End两个函数需配对使用
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string|null} [params.scriptName=null] - 脚本名,留空时将自动获取
|
||||||
|
* @param {Date} [params.startTime=new Date()] - 调用`logFakeScriptStart`时返回的Date对象
|
||||||
|
* @returns {Date} 此函数的调用时间的Date对象
|
||||||
|
* @example
|
||||||
|
* let startTime = logFakeScriptStart();
|
||||||
|
* // do something;
|
||||||
|
* logFakeScriptEnd({ startTime: startTime });
|
||||||
|
*/
|
||||||
|
function logFakeScriptEnd({ scriptName = null, startTime = new Date() } = {}) {
|
||||||
|
if (!scriptName) {
|
||||||
|
if (!scriptContext.scriptName) {
|
||||||
|
scriptContext.scriptName = getScriptName();
|
||||||
|
}
|
||||||
|
scriptName = scriptContext.scriptName;
|
||||||
|
}
|
||||||
|
return _fakeLogCore(scriptName, true, startTime);
|
||||||
|
}
|
||||||
@@ -1,107 +1,26 @@
|
|||||||
const CooldownType = {
|
eval(file.readTextSync("lib/lib.js"));
|
||||||
Unknown: "未配置刷新机制",
|
|
||||||
Every1DayMidnight: "每1天的0点",
|
|
||||||
Every2DaysMidnight: "每2天的0点",
|
|
||||||
Every3DaysMidnight: "每3天的0点",
|
|
||||||
Daily4AM: "每天凌晨4点",
|
|
||||||
Every12Hours: "12小时刷新",
|
|
||||||
Every24Hours: "24小时刷新",
|
|
||||||
Every46Hours: "46小时刷新",
|
|
||||||
};
|
|
||||||
|
|
||||||
const CooldownDataBase = {
|
|
||||||
沉玉仙茗: CooldownType.Every24Hours,
|
|
||||||
发光髓: CooldownType.Every12Hours,
|
|
||||||
蝴蝶翅膀: CooldownType.Every12Hours,
|
|
||||||
晶核: CooldownType.Every12Hours,
|
|
||||||
鳗肉: CooldownType.Every12Hours,
|
|
||||||
螃蟹: CooldownType.Every12Hours,
|
|
||||||
禽肉: CooldownType.Every12Hours,
|
|
||||||
青蛙: CooldownType.Every12Hours,
|
|
||||||
鳅鳅宝玉: CooldownType.Every12Hours,
|
|
||||||
神秘的肉: CooldownType.Every12Hours,
|
|
||||||
兽肉: CooldownType.Every12Hours,
|
|
||||||
蜥蜴尾巴: CooldownType.Every12Hours,
|
|
||||||
鱼肉: CooldownType.Every12Hours,
|
|
||||||
白萝卜: CooldownType.Every1DayMidnight,
|
|
||||||
薄荷: CooldownType.Every1DayMidnight,
|
|
||||||
澄晶实: CooldownType.Every1DayMidnight,
|
|
||||||
墩墩桃: CooldownType.Every1DayMidnight,
|
|
||||||
海草: CooldownType.Every1DayMidnight,
|
|
||||||
红果果菇: CooldownType.Every1DayMidnight,
|
|
||||||
胡萝卜: CooldownType.Every1DayMidnight,
|
|
||||||
金鱼草: CooldownType.Every1DayMidnight,
|
|
||||||
堇瓜: CooldownType.Every1DayMidnight,
|
|
||||||
烬芯花: CooldownType.Every1DayMidnight,
|
|
||||||
久雨莲: CooldownType.Every1DayMidnight,
|
|
||||||
颗粒果: CooldownType.Every1DayMidnight,
|
|
||||||
苦种: CooldownType.Every1DayMidnight,
|
|
||||||
莲蓬: CooldownType.Every1DayMidnight,
|
|
||||||
烈焰花花蕊: CooldownType.Every1DayMidnight,
|
|
||||||
马尾: CooldownType.Every1DayMidnight,
|
|
||||||
蘑菇: CooldownType.Every1DayMidnight,
|
|
||||||
茉洁草: CooldownType.Every1DayMidnight,
|
|
||||||
鸟蛋: CooldownType.Every1DayMidnight,
|
|
||||||
泡泡桔: CooldownType.Every1DayMidnight,
|
|
||||||
苹果: CooldownType.Every1DayMidnight,
|
|
||||||
日落果: CooldownType.Every1DayMidnight,
|
|
||||||
树莓: CooldownType.Every1DayMidnight,
|
|
||||||
松果: CooldownType.Every1DayMidnight,
|
|
||||||
松茸: CooldownType.Every1DayMidnight,
|
|
||||||
甜甜花: CooldownType.Every1DayMidnight,
|
|
||||||
汐藻: CooldownType.Every1DayMidnight,
|
|
||||||
香辛果: CooldownType.Every1DayMidnight,
|
|
||||||
星蕈: CooldownType.Every1DayMidnight,
|
|
||||||
须弥蔷薇: CooldownType.Every1DayMidnight,
|
|
||||||
枣椰: CooldownType.Every1DayMidnight,
|
|
||||||
竹笋: CooldownType.Every1DayMidnight,
|
|
||||||
烛伞蘑菇: CooldownType.Every1DayMidnight,
|
|
||||||
沉玉仙茗: CooldownType.Every24Hours,
|
|
||||||
晶蝶: CooldownType.Daily4AM,
|
|
||||||
|
|
||||||
铁块: CooldownType.Every1DayMidnight,
|
|
||||||
白铁块: CooldownType.Every2DaysMidnight,
|
|
||||||
电气水晶: CooldownType.Every2DaysMidnight,
|
|
||||||
星银矿石: CooldownType.Every2DaysMidnight,
|
|
||||||
萃凝晶: CooldownType.Every3DaysMidnight,
|
|
||||||
水晶块: CooldownType.Every3DaysMidnight,
|
|
||||||
紫晶块: CooldownType.Every3DaysMidnight,
|
|
||||||
奇异的龙牙: CooldownType.Every46Hours,
|
|
||||||
冰雾花: CooldownType.Every46Hours,
|
|
||||||
烈焰花: CooldownType.Every46Hours,
|
|
||||||
地方特产: CooldownType.Every46Hours,
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingFile = "settings.json";
|
const settingFile = "settings.json";
|
||||||
const baseTime = getBaseTime();
|
const defaultTime = getDefaultTime();
|
||||||
|
const CooldownDataBase = readRefreshInfo("CooldownData.txt");
|
||||||
|
|
||||||
const baseTimeStr = baseTime.toISOString();
|
|
||||||
const timeOffset = Date.parse(baseTimeStr) - Date.parse(baseTimeStr.slice(0, -1)); // 计算时区偏移量
|
|
||||||
const timeOffsetStr = offsetToTimezone(timeOffset);
|
|
||||||
let stopTime = null;
|
let stopTime = null;
|
||||||
|
let currentParty = null;
|
||||||
|
|
||||||
class ReachStopTime extends Error {
|
class ReachStopTime extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "ReachStopTime";
|
this.name = "ReachStopTime";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(async function () {
|
(async function () {
|
||||||
if (! file.IsFolder("pathing")) {
|
if (!file.IsFolder("pathing")) {
|
||||||
let batFile = "SymLink.bat";
|
let batFile = "SymLink.bat";
|
||||||
try {
|
const folderPath = getScriptDirPath();
|
||||||
file.readTextSync(`Ayaka-Main-${Math.random()}.txt`);
|
if (folderPath) {
|
||||||
} catch (error) {
|
batFile = `${folderPath}\\${batFile}`;
|
||||||
const err_msg = error.toString();
|
|
||||||
const match = err_msg.match(/'([^']+)'/);
|
|
||||||
const fullPath = match ? match[1] : null;
|
|
||||||
const folderPath = fullPath ? fullPath.replace(/\\[^\\]+$/, '') : null;
|
|
||||||
if (folderPath) {
|
|
||||||
batFile = `${folderPath}\\${batFile}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error("{0}文件夹不存在,请双击运行下列位置的脚本以创建文件夹链接\n{1}", "pathing", batFile);
|
log.error("{0}文件夹不存在,请双击运行下列位置的脚本以创建文件夹链接\n{1}", "pathing", batFile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -111,12 +30,9 @@ class ReachStopTime extends Error {
|
|||||||
if (runMode === "扫描文件夹更新可选材料列表") {
|
if (runMode === "扫描文件夹更新可选材料列表") {
|
||||||
await runScanMode();
|
await runScanMode();
|
||||||
} else if (runMode === "采集选中的材料") {
|
} else if (runMode === "采集选中的材料") {
|
||||||
const scriptName = getScriptItselfName();
|
let startTime = logFakeScriptStart();
|
||||||
// 配对关闭真正由BGI产生的那次开始记录
|
|
||||||
startTime = fakeLogCore(scriptName, true);
|
|
||||||
await runGatherMode();
|
await runGatherMode();
|
||||||
// 重新开始一条记录,与BGI产生的结束记录配对
|
logFakeScriptEnd({ startTime: startTime });
|
||||||
fakeLogCore(scriptName, true, startTime);
|
|
||||||
} else if (runMode === "清除运行记录(重置材料刷新时间)") {
|
} else if (runMode === "清除运行记录(重置材料刷新时间)") {
|
||||||
await runClearMode();
|
await runClearMode();
|
||||||
} else {
|
} else {
|
||||||
@@ -130,7 +46,7 @@ class ReachStopTime extends Error {
|
|||||||
async function runScanMode() {
|
async function runScanMode() {
|
||||||
// 1. 扫描所有最底层路径
|
// 1. 扫描所有最底层路径
|
||||||
const focusFolders = ["地方特产", "矿物", "食材与炼金"];
|
const focusFolders = ["地方特产", "矿物", "食材与炼金"];
|
||||||
const pathList = focusFolders.flatMap(fd => getLeafFolders(`pathing/${fd}`));
|
const pathList = focusFolders.flatMap((fd) => getLeafFolders(`pathing/${fd}`));
|
||||||
|
|
||||||
// 2. 读取配置模板
|
// 2. 读取配置模板
|
||||||
const templateText = file.readTextSync("settings.template.json");
|
const templateText = file.readTextSync("settings.template.json");
|
||||||
@@ -139,7 +55,7 @@ async function runScanMode() {
|
|||||||
// 将地方特产按照国家顺序排序
|
// 将地方特产按照国家顺序排序
|
||||||
const countryList = ["蒙德", "璃月", "稻妻", "须弥", "枫丹", "纳塔", "至冬"];
|
const countryList = ["蒙德", "璃月", "稻妻", "须弥", "枫丹", "纳塔", "至冬"];
|
||||||
const sortedList = pathList.slice().sort((a, b) => {
|
const sortedList = pathList.slice().sort((a, b) => {
|
||||||
const getRegion = p => p.split("\\")[2];
|
const getRegion = (p) => p.split("\\")[2];
|
||||||
const aIndex = countryList.indexOf(getRegion(a));
|
const aIndex = countryList.indexOf(getRegion(a));
|
||||||
const bIndex = countryList.indexOf(getRegion(b));
|
const bIndex = countryList.indexOf(getRegion(b));
|
||||||
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
|
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
|
||||||
@@ -147,29 +63,37 @@ async function runScanMode() {
|
|||||||
|
|
||||||
// 3. 处理每个路径
|
// 3. 处理每个路径
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
actions_map = {};
|
||||||
for (const path of sortedList) {
|
for (const path of sortedList) {
|
||||||
const info = getCooldownInfoFromPath(path);
|
const info = getCooldownInfoFromPath(path);
|
||||||
const jsonFiles = filterFilesInTaskDir(info.label);
|
const jsonFiles = filterFilesInTaskDir(info.label);
|
||||||
|
|
||||||
if (jsonFiles.length === 0) {
|
if (jsonFiles.length === 0) {
|
||||||
log.info("{0}内无json文件,跳过", path);
|
log.info("{0}内无json文件,跳过", path);
|
||||||
} else if (info.coolType === CooldownType.Unknown) {
|
} else if (info.coolType === null) {
|
||||||
log.warn("路径{0}未找到对应的刷新机制,跳过", path);
|
log.warn("路径{0}未匹配到对应的刷新机制,跳过", path);
|
||||||
} else {
|
} else {
|
||||||
config.push({
|
config.push({
|
||||||
name: info.name,
|
name: info.name,
|
||||||
label: "⬇️ " + info.label,
|
label: "⬇️ " + info.label,
|
||||||
type: "checkbox"
|
type: "checkbox",
|
||||||
});
|
});
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
|
const actions = scanSpecialCollectMethod(jsonFiles);
|
||||||
|
if (actions.length > 0) {
|
||||||
|
actions_map[path] = actions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 4. 写入新的配置(格式化输出)
|
// 4. 写入新的配置(格式化输出)
|
||||||
file.writeTextSync(settingFile, JSON.stringify(config, null, 2));
|
file.writeTextSync(settingFile, JSON.stringify(config, null, 2));
|
||||||
log.info("共{0}组有效路线,请在脚本配置中勾选需要采集的材料", count);
|
log.info("共{0}组有效路线,请在脚本配置中勾选需要采集的材料", count);
|
||||||
|
// 5. 分析所需角色信息
|
||||||
|
analysisCharacterRequirement(actions_map);
|
||||||
|
await sleep(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 采集选中的材料
|
// 采集选中的材料
|
||||||
async function runGatherMode() {
|
async function runGatherMode() {
|
||||||
const selectedMaterials = getSelectedMaterials();
|
const selectedMaterials = getSelectedMaterials();
|
||||||
@@ -178,33 +102,22 @@ async function runGatherMode() {
|
|||||||
log.error("未选择任何材料,请在脚本配置中勾选所需项目");
|
log.error("未选择任何材料,请在脚本配置中勾选所需项目");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (settings.stopAtTime) {
|
if (settings.Time) {
|
||||||
stopTime = calcStopTime(settings.stopAtTime);
|
stopTime = settings.stopAtTime;
|
||||||
log.info("脚本已被配置为达到{0}后停止运行", strftime(stopTime, true));
|
log.info("脚本已被配置为达到{0}后停止运行", stopTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("共{0}组材料路线待执行:", selectedMaterials.length);
|
log.info("共{0}组材料路线待执行:", selectedMaterials.length);
|
||||||
for (const item of selectedMaterials) {
|
for (const item of selectedMaterials) {
|
||||||
const info = getCooldownInfoFromPath(item.label);
|
const info = getCooldownInfoFromPath(item.label);
|
||||||
log.info(` - {0} (${info.coolType})`, item.label || item.name);
|
log.info(` - {0} (${info.coolType}刷新)`, item.label || item.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let account = await getCurrentAccount();
|
let account = await getGameAccount(settings.iHaveMultipleAccounts);
|
||||||
log.info("为{0}采集材料并管理CD", account);
|
log.info("为{0}采集材料并管理CD", account);
|
||||||
|
|
||||||
if (settings.partyName) {
|
await switchPartySafely(settings.partyName);
|
||||||
try {
|
currentParty = settings.partyName;
|
||||||
if (!(await genshin.switchParty(settings.partyName))) {
|
|
||||||
log.info("切换队伍失败,前往七天神像重试");
|
|
||||||
await genshin.tpToStatueOfTheSeven();
|
|
||||||
await sleep(1000);
|
|
||||||
await genshin.switchParty(settings.partyName);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
log.error("队伍切换失败,可能处于联机模式或其他不可切换状态");
|
|
||||||
await genshin.returnMainUi();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
|
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
|
||||||
// 可在此处继续处理 selectedMaterials 列表
|
// 可在此处继续处理 selectedMaterials 列表
|
||||||
@@ -214,14 +127,13 @@ async function runGatherMode() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ReachStopTime) {
|
if (e instanceof ReachStopTime) {
|
||||||
log.info("达到设置的停止时间 {0},终止运行", strftime(stopTime, true));
|
log.info("达到设置的停止时间 {0},终止运行", stopTime);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 清除运行记录(重置材料刷新时间)
|
// 清除运行记录(重置材料刷新时间)
|
||||||
async function runClearMode() {
|
async function runClearMode() {
|
||||||
const selectedMaterials = getSelectedMaterials();
|
const selectedMaterials = getSelectedMaterials();
|
||||||
@@ -229,13 +141,13 @@ async function runClearMode() {
|
|||||||
if (selectedMaterials.length === 0) {
|
if (selectedMaterials.length === 0) {
|
||||||
log.error("未选择任何材料,请在脚本配置中勾选所需项目");
|
log.error("未选择任何材料,请在脚本配置中勾选所需项目");
|
||||||
}
|
}
|
||||||
const resetTime = strftime(baseTime);
|
const resetTimeStr = formatDateTime(getDefaultTime());
|
||||||
let account = await getCurrentAccount();
|
let account = await getGameAccount(settings.iHaveMultipleAccounts);
|
||||||
for (const pathTask of selectedMaterials) {
|
for (const pathTask of selectedMaterials) {
|
||||||
const jsonFiles = filterFilesInTaskDir(pathTask.label);
|
const jsonFiles = filterFilesInTaskDir(pathTask.label);
|
||||||
const recordFile = getRecordFilePath(account, pathTask);
|
const recordFile = getRecordFilePath(account, pathTask);
|
||||||
const lines = jsonFiles.map((filePath) => {
|
const lines = jsonFiles.map((filePath) => {
|
||||||
return `${basename(filePath)}\t${resetTime}`;
|
return `${basename(filePath)}\t${resetTimeStr}`;
|
||||||
});
|
});
|
||||||
const content = lines.join("\n");
|
const content = lines.join("\n");
|
||||||
file.writeTextSync(recordFile, content);
|
file.writeTextSync(recordFile, content);
|
||||||
@@ -244,25 +156,94 @@ async function runClearMode() {
|
|||||||
log.info("已重置{0}组刷新时间。如需重置所有材料刷新时间,请直接删除record目录下对应账号的文件夹", selectedMaterials.length);
|
log.info("已重置{0}组刷新时间。如需重置所有材料刷新时间,请直接删除record目录下对应账号的文件夹", selectedMaterials.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scanSpecialCollectMethod(jsonFiles) {
|
||||||
|
const actions = jsonFiles.flatMap((filePath) => {
|
||||||
|
const data = JSON.parse(file.readTextSync(filePath));
|
||||||
|
return data.positions.map((p) => p.action).filter((a) => a);
|
||||||
|
});
|
||||||
|
return [...new Set(actions)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRefreshInfo(filePath) {
|
||||||
|
const lines = file.readTextSync(filePath).split(/\r?\n/);
|
||||||
|
const dict = {};
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue; // 跳过空行
|
||||||
|
const [key, value] = line.split(":");
|
||||||
|
dict[key.trim()] = value ? value.trim() : "";
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function analysisCharacterRequirement(actions_map) {
|
||||||
|
const result = {};
|
||||||
|
for (const [key, values] of Object.entries(actions_map)) {
|
||||||
|
const newKey = key.replace(/^pathing\\/, "");
|
||||||
|
for (const v of values) {
|
||||||
|
if (!result[v]) {
|
||||||
|
result[v] = [];
|
||||||
|
}
|
||||||
|
result[v].push(newKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let collect_methods = {};
|
||||||
|
for (const [key, value] of Object.entries(result)) {
|
||||||
|
if (key.endsWith("_collect") || key === "fight" || key === "combat_script") {
|
||||||
|
collect_methods[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_methods = Object.fromEntries(Object.entries(collect_methods).sort((a, b) => b[1].length - a[1].length));
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"角色需求: {1}条路线需要纳西妲,{2}条路线需要水元素,{3}条路线需要雷元素,{4}条路线需要风元素,{5}条路线需要火元素;{6}条路线需要执行自动战斗;{7}条路线使用了战斗策略脚本(含挖矿等非战斗用途)",
|
||||||
|
collect_methods["nahida_collect"]?.length || 0,
|
||||||
|
collect_methods["hydro_collect"]?.length || 0,
|
||||||
|
collect_methods["electro_collect"]?.length || 0,
|
||||||
|
collect_methods["anemo_collect"]?.length || 0,
|
||||||
|
collect_methods["pyro_collect"]?.length || 0,
|
||||||
|
collect_methods["fight"]?.length || 0,
|
||||||
|
collect_methods["combat_script"]?.length || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const nameMap = {
|
||||||
|
nahida_collect: "纳西妲",
|
||||||
|
hydro_collect: "水元素",
|
||||||
|
electro_collect: "雷元素",
|
||||||
|
anemo_collect: "风元素",
|
||||||
|
pyro_collect: "火元素",
|
||||||
|
fight: "自动战斗",
|
||||||
|
combat_script: "战斗策略脚本",
|
||||||
|
};
|
||||||
|
|
||||||
|
let analysisResult = {};
|
||||||
|
for (const [key, value] of Object.entries(collect_methods)) {
|
||||||
|
const name = nameMap[key] || key;
|
||||||
|
analysisResult[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outFile = `${getScriptDirPath()}\\各条路线所需角色.txt`;
|
||||||
|
let text = "";
|
||||||
|
// text = JSON.stringify(analysisResult, null, 2);
|
||||||
|
for (const [key, values] of Object.entries(analysisResult)) {
|
||||||
|
text += `${key}\n`;
|
||||||
|
for (const v of values) {
|
||||||
|
text += ` ${v}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.writeTextSync(outFile, text);
|
||||||
|
log.info("详细路线需求见{x},可考虑组两支队伍{0}和{1}以满足采集需要", outFile, "钟纳水雷", "钟纳火风");
|
||||||
|
}
|
||||||
|
|
||||||
function getRecordFilePath(account, pathTask) {
|
function getRecordFilePath(account, pathTask) {
|
||||||
const taskName = pathTask.name.replace(/^OPT_/, "");
|
const taskName = pathTask.name.replace(/^OPT_/, "");
|
||||||
return `record/${account}/${taskName}.txt`;
|
return `record/${account}/${taskName}.txt`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterFilesInTaskDir(taskDir, ext=".json") {
|
function filterFilesInTaskDir(taskDir) {
|
||||||
const allFilesRaw = file.ReadPathSync("pathing\\" + taskDir);
|
return getFilesByExtension("pathing\\" + taskDir, ".json");
|
||||||
const extFiles = [];
|
|
||||||
|
|
||||||
for (const filePath of allFilesRaw) {
|
|
||||||
if (filePath.endsWith(ext)) {
|
|
||||||
extFiles.push(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return extFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function runPathTaskIfCooldownExpired(account, pathTask) {
|
async function runPathTaskIfCooldownExpired(account, pathTask) {
|
||||||
const recordFile = getRecordFilePath(account, pathTask);
|
const recordFile = getRecordFilePath(account, pathTask);
|
||||||
const jsonFiles = filterFilesInTaskDir(pathTask.label);
|
const jsonFiles = filterFilesInTaskDir(pathTask.label);
|
||||||
@@ -287,44 +268,51 @@ async function runPathTaskIfCooldownExpired(account, pathTask) {
|
|||||||
for (let i = 0; i < jsonFiles.length; i++) {
|
for (let i = 0; i < jsonFiles.length; i++) {
|
||||||
const jsonPath = jsonFiles[i];
|
const jsonPath = jsonFiles[i];
|
||||||
const fileName = basename(jsonPath);
|
const fileName = basename(jsonPath);
|
||||||
const lastTime = recordMap[fileName] || baseTime;
|
|
||||||
const pathName = fileName.split(".")[0];
|
const pathName = fileName.split(".")[0];
|
||||||
|
const lastTime = recordMap[fileName] || defaultTime;
|
||||||
const progress = `[${i + 1}/${jsonFiles.length}]`;
|
const progress = `[${i + 1}/${jsonFiles.length}]`;
|
||||||
|
|
||||||
if (stopTime && Date.now() >= stopTime) {
|
if (settings.Time && isTargetTimeReached(stopTime)) {
|
||||||
throw new ReachStopTime("达到设置的停止时间,终止运行");
|
throw new ReachStopTime("达到设置的停止时间,终止运行");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() > lastTime) {
|
if (Date.now() > lastTime) {
|
||||||
let pathStart = addFakePathLog(fileName);
|
let pathStart = logFakePathStart(fileName);
|
||||||
log.info(`${progress}{0}: 开始执行`, pathName);
|
log.info(`${progress}{0}: 开始执行`, pathName);
|
||||||
|
|
||||||
let pathStartTime = new Date();
|
let pathStartTime = new Date();
|
||||||
try {
|
try {
|
||||||
await pathingScript.runFile(jsonPath);
|
await pathingScript.runFile(jsonPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(`${progress}{0}: 文件不存在或执行失败: {1}`, pathName, error.toString());
|
log.error(`${progress}{0}: 文件不存在或执行失败: {1}`, jsonPath, error.toString());
|
||||||
addFakePathLog(fileName, pathStart);
|
logFakePathEnd(fileName, pathStart);
|
||||||
continue; // 跳过当前任务
|
continue; // 跳过当前任务
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新记录
|
let diffTime = new Date() - pathStartTime;
|
||||||
if (new Date() - pathStartTime > 5000) {
|
if (diffTime < 500) {
|
||||||
|
// "队伍中没有对应元素角色"的错误不会抛出为异常,只能通过路径文件迅速结束来推测
|
||||||
|
if (settings.partyName && settings.partyName2nd) {
|
||||||
|
let newParty = (currentParty === settings.partyName) ? settings.partyName2nd : settings.partyName;
|
||||||
|
log.info("当前队伍{0}缺少该路线所需角色,尝试切换到{1}", currentParty, newParty);
|
||||||
|
await switchPartySafely(newParty);
|
||||||
|
await pathingScript.runFile(jsonPath);
|
||||||
|
}
|
||||||
|
} else if (diffTime > 5000) {
|
||||||
recordMap[fileName] = calculateNextRunTime(new Date(), jsonPath);
|
recordMap[fileName] = calculateNextRunTime(new Date(), jsonPath);
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
|
||||||
for (const [p, t] of Object.entries(recordMap)) {
|
for (const [p, t] of Object.entries(recordMap)) {
|
||||||
lines.push(`${p}\t${strftime(t)}`);
|
lines.push(`${p}\t${formatDateTime(t)}`);
|
||||||
}
|
}
|
||||||
const content = lines.join("\n");
|
const content = lines.join("\n");
|
||||||
file.writeTextSync(recordFile, content);
|
file.writeTextSync(recordFile, content);
|
||||||
log.info(`${progress}{0}: 已完成,下次刷新: ${strftime(recordMap[fileName], true)}`, pathName);
|
log.info(`${progress}{0}: 已完成,下次刷新: ${formatDateTimeShort(recordMap[fileName])}`, pathName);
|
||||||
} else {
|
} else {
|
||||||
log.info(`${progress}{0}: 执行时间过短,不更新记录`, pathName);
|
log.info(`${progress}{0}: 执行时间过短,不更新记录`, pathName);
|
||||||
}
|
}
|
||||||
addFakePathLog(fileName, pathStart);
|
logFakePathEnd(fileName, pathStart);
|
||||||
} else {
|
} else {
|
||||||
log.info(`${progress}{0}: 已跳过 (${strftime(recordMap[fileName], true)}刷新)`, pathName);
|
log.info(`${progress}{0}: 已跳过 (${formatDateTimeShort(recordMap[fileName])}刷新)`, pathName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +324,7 @@ async function runPathTaskIfCooldownExpired(account, pathTask) {
|
|||||||
*/
|
*/
|
||||||
function getCooldownInfoFromPath(fullPath) {
|
function getCooldownInfoFromPath(fullPath) {
|
||||||
const parts = fullPath.split(/[\\/]/); // 支持 \ 或 / 分隔符
|
const parts = fullPath.split(/[\\/]/); // 支持 \ 或 / 分隔符
|
||||||
let cooldown = CooldownType.Unknown;
|
let cooldown = null;
|
||||||
let cleanPart = "";
|
let cleanPart = "";
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@@ -359,123 +347,11 @@ function getCooldownInfoFromPath(fullPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calculateNextRunTime(base, fullPath) {
|
function calculateNextRunTime(base, fullPath) {
|
||||||
const {coolType} = getCooldownInfoFromPath(fullPath);
|
const { coolType } = getCooldownInfoFromPath(fullPath);
|
||||||
let nextTime = baseTime;
|
let nextTime = calculateNextRefreshTime(base, coolType);
|
||||||
|
|
||||||
switch (coolType) {
|
|
||||||
case CooldownType.Every1DayMidnight: {
|
|
||||||
const next = new Date(base.getTime() + 1 * 24 * 60 * 60 * 1000);
|
|
||||||
next.setHours(0, 0, 0, 0);
|
|
||||||
nextTime = next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CooldownType.Every2DaysMidnight: {
|
|
||||||
const next = new Date(base.getTime() + 2 * 24 * 60 * 60 * 1000);
|
|
||||||
next.setHours(0, 0, 0, 0);
|
|
||||||
nextTime = next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CooldownType.Every3DaysMidnight: {
|
|
||||||
const next = new Date(base.getTime() + 3 * 24 * 60 * 60 * 1000);
|
|
||||||
next.setHours(0, 0, 0, 0);
|
|
||||||
nextTime = next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CooldownType.Daily4AM: {
|
|
||||||
const next = new Date(base);
|
|
||||||
next.setHours(4, 0, 0, 0);
|
|
||||||
if (base.getHours() >= 4) {
|
|
||||||
// 如果已过今天凌晨4点,则设为明天的4点
|
|
||||||
next.setDate(next.getDate() + 1);
|
|
||||||
}
|
|
||||||
nextTime = next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CooldownType.Every12Hours: {
|
|
||||||
nextTime = new Date(base.getTime() + 12 * 60 * 60 * 1000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CooldownType.Every24Hours: {
|
|
||||||
nextTime = new Date(base.getTime() + 24 * 60 * 60 * 1000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CooldownType.Every46Hours: {
|
|
||||||
nextTime = new Date(base.getTime() + 46 * 60 * 60 * 1000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`未识别的冷却类型: ${coolType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextTime;
|
return nextTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定路径下所有最底层的文件夹(即不包含任何子文件夹的文件夹)
|
|
||||||
* @param {string} folderPath - 要遍历的根文件夹路径
|
|
||||||
* @param {string[]} result - 用于收集最底层文件夹路径的数组
|
|
||||||
* @returns {Promise<string[]>} 所有最底层文件夹的路径
|
|
||||||
*/
|
|
||||||
function getLeafFolders(folderPath, result = []) {
|
|
||||||
const filesInSubFolder = file.ReadPathSync(folderPath);
|
|
||||||
let hasSubFolder = false;
|
|
||||||
|
|
||||||
for (const filePath of filesInSubFolder) {
|
|
||||||
if (file.IsFolder(filePath)) {
|
|
||||||
hasSubFolder = true;
|
|
||||||
// 递归查找子文件夹
|
|
||||||
getLeafFolders(filePath, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有发现任何子文件夹,则当前为最底层文件夹
|
|
||||||
if (!hasSubFolder) {
|
|
||||||
result.push(folderPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCurrentAccount() {
|
|
||||||
let account = "默认账号";
|
|
||||||
|
|
||||||
if (settings.iHaveMultipleAccounts) {
|
|
||||||
// 打开背包避免界面背景干扰
|
|
||||||
await genshin.returnMainUi();
|
|
||||||
keyPress("B");
|
|
||||||
await sleep(1000);
|
|
||||||
|
|
||||||
const region = captureGameRegion();
|
|
||||||
const ocrResults = RecognitionObject.ocr(region.width * 0.75, region.height * 0.75, region.width * 0.25, region.height * 0.25);
|
|
||||||
const resList = region.findMulti(ocrResults);
|
|
||||||
|
|
||||||
for (let i = 0; i < resList.count; i++) {
|
|
||||||
const text = resList[i].text;
|
|
||||||
if (text.includes("UID")) {
|
|
||||||
const match = text.match(/\d+/);
|
|
||||||
if (match) {
|
|
||||||
account = match[0];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account === "默认账号") {
|
|
||||||
log.error("未能提取到UID");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedMaterials() {
|
function getSelectedMaterials() {
|
||||||
const configText = file.readTextSync(settingFile);
|
const configText = file.readTextSync(settingFile);
|
||||||
const config = JSON.parse(configText); // 配置数组
|
const config = JSON.parse(configText); // 配置数组
|
||||||
@@ -483,120 +359,62 @@ function getSelectedMaterials() {
|
|||||||
const selectedMaterials = [];
|
const selectedMaterials = [];
|
||||||
|
|
||||||
for (const entry of config) {
|
for (const entry of config) {
|
||||||
if (
|
if (entry.name && entry.name.startsWith("OPT_") && entry.type === "checkbox") {
|
||||||
entry.name &&
|
if (settings.selectAllMaterials || settings[entry.name] === true) {
|
||||||
entry.name.startsWith("OPT_") &&
|
let index = entry.label.indexOf(" ");
|
||||||
entry.type === "checkbox"
|
entry.label = entry.label.slice(index + 1); // 去除⬇️指示
|
||||||
) {
|
|
||||||
if (settings[entry.name] === true) {
|
|
||||||
entry.label = entry.label.split(" ")[1]; // 去除⬇️指示
|
|
||||||
selectedMaterials.push(entry);
|
selectedMaterials.push(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const materialDict = {};
|
||||||
|
selectedMaterials.forEach((item) => {
|
||||||
|
const label = item.label;
|
||||||
|
const match = label.match(/\\(.*?)\\\1/); // \落落莓\落落莓@Author
|
||||||
|
let materialName;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
materialName = match[1];
|
||||||
|
} else {
|
||||||
|
const parts = label.split("\\");
|
||||||
|
materialName = parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!materialDict[materialName]) {
|
||||||
|
materialDict[materialName] = [];
|
||||||
|
}
|
||||||
|
materialDict[materialName].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRoutes = [];
|
||||||
|
const multiRoutes = {};
|
||||||
|
for (const materialName in materialDict) {
|
||||||
|
const routes = materialDict[materialName];
|
||||||
|
if (routes.length > 0) {
|
||||||
|
firstRoutes.push(routes[0]);
|
||||||
|
if (materialDict[materialName].length > 1) {
|
||||||
|
multiRoutes[materialName] = materialDict[materialName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const countOfMultiRoutes = Object.keys(multiRoutes).length;
|
||||||
|
if (countOfMultiRoutes > 0) {
|
||||||
|
let text = `${countOfMultiRoutes}种材料存在多个版本的路线:\n`;
|
||||||
|
for (const [key, values] of Object.entries(multiRoutes)) {
|
||||||
|
text += `${key}\n`;
|
||||||
|
for (const v of values) {
|
||||||
|
text += ` ${v.label}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug(text);
|
||||||
|
if (settings.acceptMultiplePathOfSameMaterial) {
|
||||||
|
log.warn("{0}种材料选中了多个版本的路线(详见日志文件),根据脚本设置,将执行全部版本", countOfMultiRoutes);
|
||||||
|
} else {
|
||||||
|
log.warn("{0}种材料选中了多个版本的路线(详见日志文件),默认只执行第一个版本", countOfMultiRoutes);
|
||||||
|
return firstRoutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return selectedMaterials;
|
return selectedMaterials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Happy Birthday
|
|
||||||
function getBaseTime() {
|
|
||||||
const now = new Date();
|
|
||||||
const year = now.getFullYear() - 18;
|
|
||||||
return new Date(year, 8, 28, 0, 0, 0); // 9月是month=8(0起始)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function strftime(dateObj, shortFormat = false) {
|
|
||||||
const timestamp = dateObj.getTime() + timeOffset;
|
|
||||||
const newDate = new Date(timestamp);
|
|
||||||
let s = newDate.toISOString();
|
|
||||||
s = s.replace("Z", timeOffsetStr);
|
|
||||||
|
|
||||||
if (shortFormat) {
|
|
||||||
// 截取出 MM-DD HH:MM:SS
|
|
||||||
const [datePart, timePart] = s.split("T");
|
|
||||||
const [year, month, day] = datePart.split("-");
|
|
||||||
const time = timePart.split(".")[0]; // 去掉毫秒部分
|
|
||||||
s = `${month}-${day} ${time}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function basename(filePath) {
|
|
||||||
const lastSlashIndex = filePath.lastIndexOf('\\');
|
|
||||||
return filePath.substring(lastSlashIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function offsetToTimezone(offsetMs) {
|
|
||||||
const totalMinutes = offsetMs / (1000 * 60);
|
|
||||||
const sign = totalMinutes >= 0 ? "+" : "-";
|
|
||||||
const absMinutes = Math.abs(totalMinutes);
|
|
||||||
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
|
|
||||||
const minutes = String(absMinutes % 60).padStart(2, "0");
|
|
||||||
return `${sign}${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcStopTime(timeStr) {
|
|
||||||
const match = timeStr.match(/\b\d{2}[::]\d{2}\b/); // 匹配 HH:mm
|
|
||||||
if (!match) {
|
|
||||||
return new Date(0xFFFFFFFF * 1000); // 不停止
|
|
||||||
}
|
|
||||||
|
|
||||||
const [hour, minute] = match[0].split(":").map(Number);
|
|
||||||
const now = new Date();
|
|
||||||
const next = new Date(now);
|
|
||||||
|
|
||||||
next.setHours(hour, minute, 0, 0);
|
|
||||||
if (next <= now) {
|
|
||||||
next.setDate(next.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScriptItselfName() {
|
|
||||||
const content = file.readTextSync("manifest.json");
|
|
||||||
const manifest = JSON.parse(content);
|
|
||||||
return manifest.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 参考了 mno 大佬的函数
|
|
||||||
|
|
||||||
function fakeLogCore(name, isJs = true, dateIn = null) {
|
|
||||||
const isStart = (isJs === (dateIn !== null));
|
|
||||||
const lastRun = isJs ? new Date() : dateIn;
|
|
||||||
const task = isJs ? "JS脚本" : "地图追踪任务";
|
|
||||||
let logMessage = "";
|
|
||||||
let logTime = new Date();
|
|
||||||
if (isJs && isStart) {
|
|
||||||
logTime = dateIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logTimeWithOffset = new Date(logTime.getTime() + timeOffset);
|
|
||||||
const formattedTime = logTimeWithOffset.toISOString().split("T")[1].replace("Z", "");
|
|
||||||
|
|
||||||
if (isStart) {
|
|
||||||
logMessage = `正在伪造开始的日志记录\n\n` +
|
|
||||||
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
|
||||||
`------------------------------\n\n` +
|
|
||||||
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
|
||||||
`→ 开始执行${task}: "${name}"`;
|
|
||||||
} else {
|
|
||||||
const durationInSeconds = (logTime.getTime() - lastRun.getTime()) / 1000;
|
|
||||||
const durationMinutes = Math.floor(durationInSeconds / 60);
|
|
||||||
const durationSeconds = (durationInSeconds % 60).toFixed(3); // 保留三位小数
|
|
||||||
logMessage = `正在伪造结束的日志记录\n\n` +
|
|
||||||
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
|
||||||
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}分${durationSeconds}秒\n\n` +
|
|
||||||
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
|
|
||||||
`------------------------------`;
|
|
||||||
}
|
|
||||||
log.debug(logMessage);
|
|
||||||
return logTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFakePathLog(name, lastRun = null) {
|
|
||||||
return fakeLogCore(name, false, lastRun);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 1,
|
"manifest_version": 1,
|
||||||
"name": "带CD管理的自动采集",
|
"name": "带CD管理的自动采集",
|
||||||
"version": "1.1",
|
"version": "1.2",
|
||||||
"bgi_version": "0.45.0",
|
"bgi_version": "0.45.0",
|
||||||
"description": "自动同步你通过BetterGI订阅的地图追踪任务,执行采集任务,并管理材料刷新时间(支持多账号)。\n首次运行前请先简单阅读说明,推荐在线版 https://gitee.com/babalae/bettergi-scripts-list/tree/main/repo/js/CD-Aware-AutoGather \n本地版说明见脚本目录内的 README.md 文件",
|
"description": "自动同步你通过BetterGI订阅的地图追踪任务,执行采集任务,并管理材料刷新时间(支持多账号)。\n首次使用前请先简单阅读说明(可在`全自动`——`JS脚本`页面,点击本脚本名称查看)",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Ayaka-Main",
|
"name": "Ayaka-Main",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"name": "runMode",
|
"name": "runMode",
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"label": "首次运行前请先简单阅读说明,推荐在线版\n https://gitee.com/babalae/bettergi-scripts-list\n/tree/main/repo/js/CD-Aware-AutoGather \n本地版说明见脚本目录内的 README.md 文件",
|
"label": "首次使用前请先简单阅读说明(可在`全自动`——`JS脚本`页面,点击本脚本名称查看)",
|
||||||
"options": [
|
"options": [
|
||||||
"扫描文件夹更新可选材料列表"
|
"扫描文件夹更新可选材料列表"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,7 +12,12 @@
|
|||||||
{
|
{
|
||||||
"name": "partyName",
|
"name": "partyName",
|
||||||
"type": "input-text",
|
"type": "input-text",
|
||||||
"label": "设置要使用的队伍名称(留空则不进行切换)"
|
"label": "设置首选队伍名称(留空则不进行切换)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "partyName2nd",
|
||||||
|
"type": "input-text",
|
||||||
|
"label": "设置备选队伍名称(首选队伍缺少对应的采集角色时使用)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "stopAtTime",
|
"name": "stopAtTime",
|
||||||
@@ -23,5 +28,15 @@
|
|||||||
"name": "iHaveMultipleAccounts",
|
"name": "iHaveMultipleAccounts",
|
||||||
"type": "checkbox",
|
"type": "checkbox",
|
||||||
"label": "我肝的账号不止一个(选中后将分账号维护对应的材料刷新时间)"
|
"label": "我肝的账号不止一个(选中后将分账号维护对应的材料刷新时间)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "selectAllMaterials",
|
||||||
|
"type": "checkbox",
|
||||||
|
"label": "采集扫描到的所有材料(选中后将无视后面的每个材料⬇️是否选中)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "acceptMultiplePathOfSameMaterial",
|
||||||
|
"type": "checkbox",
|
||||||
|
"label": "即使同一种材料有多个版本的路线,也全都执行采集"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user