diff --git a/repo/js/CD-Aware-AutoGather/README.md b/repo/js/CD-Aware-AutoGather/README.md
new file mode 100644
index 00000000..97800017
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/README.md
@@ -0,0 +1,56 @@
+# 使用前准备
+
+**双击运行脚本所在目录下的`SymLink.bat`文件,以创建符号链接。**
+
+此操作只需要一次。运行后,脚本下的`pathing`文件夹将指向Better GI的地图追踪文件夹`User\AutoPathing`,你通过Better GI新增或删除地图追踪任务后,脚本这边看到的也是修改后的。
+
+# 运行模式
+
+必须先完成`使用前准备`里的操作
+
+## 1. 扫描文件夹更新可选材料列表
+
+扫描脚本`pathing`目录(等价于你在Better GI里订阅的地图追踪任务目录)下的`地方特产`、`矿物`、`食材与炼金`内的材料,并自动匹配材料的刷新时间。
+
+扫描完成后,将自动更新脚本可用的配置菜单。此时再次打开右键的`修改JS脚本自定义配置`,将看到新增了多个配置项,其中包含刚刚扫描到的材料目录。
+
+
+
+如果你订阅了很多地图追踪任务,那么扫描结果也会比较多,选项列表也会比较长,但不影响脚本运行。
+
+只有你新增或者删除了地图追踪任务的订阅时,才需要运行此模式。
+
+## 2. 采集选中的材料
+
+此模式下有这些选项可以配置:
+
+| 选项 | 说明 |
+| ---- | ---- |
+| 设置要使用的队伍名称 | 执行采集任务前切换到指定的队伍,未设置则不切换。 |
+| 停止运行时间 | 超过此时间后,停止后续的任务(会等待已经运行中的json路线结束)。 |
+| 我肝的账号不止一个 | 如果你有多个账号,可以选中此选项,选中后将分账号维护对应的材料刷新时间。 |
+| `↓` 地方特产\稻妻\绯樱绣球 | 根据你订阅的路径追踪任务数量,这里将会显示相应个数的选择框。
勾选后将执行你选中的条目的采集任务。
Tip: `↓`符号是在提示你应该勾选文本下面的选择框 |
+
+运行此模式后,将按照你勾选的条目,执行相应的采集任务。每执行完一条json路线后,将会计算它的下次刷新时间并写入`record`文件夹下的记录文件。下次运行脚本时,未刷新的路线将自动跳过。
+
+可以同时勾选多种材料,会逐个进行采集。
+
+如果不同的采集任务需要不同队伍,那请在调度器配置组里添加多次本脚本,然后分别设置不同的采集物和采集队伍。
+
+支持使用配置组`更多功能`——`日志分析`分析运行记录(参考了[mno](https://github.com/Bedrockx)大佬的写法)。
+
+
+
+## 3. 清除运行记录(重置材料刷新时间)
+
+此模式下,相关的选项只有`我肝的账号不止一个`和以`↓`开头的任务名,作用同上文。
+
+运行此模式后,将重置你选中的任务相应材料的刷新时间。
+
+如果你需要删除全部运行记录,可以直接删除脚本`record`文件夹下,以账号为名称的文件夹内的文件。
+
+---
+
+***想要参与改进此脚本?***
+
+脚本目录下有个`脚本思路.txt`文件,先看看它可以快速理解我开发时的思路,易于快速上手。
diff --git a/repo/js/CD-Aware-AutoGather/SymLink.bat b/repo/js/CD-Aware-AutoGather/SymLink.bat
new file mode 100644
index 00000000..757fb86a
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/SymLink.bat
@@ -0,0 +1,12 @@
+@REM @echo off
+set "target1=..\..\AutoPathing"
+set "target2=..\..\pathing"
+
+if exist "%target1%" (
+ mklink /j pathing "%target1%"
+) else if exist "%target2%" (
+ mklink /j pathing "%target2%"
+) else (
+ echo ERROR: Can't find folder "%target1%" or "%target2%"
+ pause
+)
diff --git a/repo/js/CD-Aware-AutoGather/log_analysis.png b/repo/js/CD-Aware-AutoGather/log_analysis.png
new file mode 100644
index 00000000..30573d21
Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/log_analysis.png differ
diff --git a/repo/js/CD-Aware-AutoGather/main.js b/repo/js/CD-Aware-AutoGather/main.js
new file mode 100644
index 00000000..3c44c284
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/main.js
@@ -0,0 +1,592 @@
+const CooldownType = {
+ 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 baseTime = getBaseTime();
+
+const baseTimeStr = baseTime.toISOString();
+const timeOffset = Date.parse(baseTimeStr) - Date.parse(baseTimeStr.slice(0, -1)); // 计算时区偏移量
+const timeOffsetStr = offsetToTimezone(timeOffset);
+let stopTime = null;
+
+class ReachStopTime extends Error {
+ constructor(message) {
+ super(message);
+ this.name = "ReachStopTime";
+ }
+}
+
+(async function () {
+ if (! file.IsFolder("pathing")) {
+ let batFile = "SymLink.bat";
+ 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;
+ if (folderPath) {
+ batFile = `${folderPath}\\${batFile}`;
+ }
+ }
+
+ log.error("{0}文件夹不存在,请双击运行下列位置的脚本以创建文件夹链接\n{1}", "pathing", batFile);
+ return;
+ }
+ const runMode = settings.runMode;
+
+ const scriptName = getScriptItselfName();
+ // 结束真正由BGI产生的那次开始记录
+ startTime = fakeLogCore(scriptName, true);
+
+ log.info("当前运行模式:{0}", runMode);
+ if (runMode === "扫描文件夹更新可选材料列表") {
+ await runScanMode();
+ } else if (runMode === "采集选中的材料") {
+ await runGatherMode();
+ } else if (runMode === "清除运行记录(重置材料刷新时间)") {
+ await runClearMode();
+ } else {
+ log.warn("未选择运行模式或运行模式无效: {0}\n这可能是你的首次运行,将为你执行{1}模式", runMode, "扫描文件夹更新可选材料列表");
+ await sleep(3000);
+ await runScanMode();
+ }
+ // 重新开始一条记录,与BGI产生的结束记录配对
+ fakeLogCore(scriptName, true, startTime);
+})();
+
+// 扫描文件夹更新可选材料列表
+async function runScanMode() {
+ // 1. 扫描所有最底层路径
+ const focusFolders = ["地方特产", "矿物", "食材与炼金"];
+ const pathList = focusFolders.flatMap(fd => getLeafFolders(`pathing/${fd}`));
+
+ // 2. 读取配置模板
+ const templateText = file.readTextSync("settings.template.json");
+ let config = JSON.parse(templateText);
+
+ // 3. 处理每个路径
+ let count = 0;
+ for (const path of pathList) {
+ const info = getCooldownInfoFromPath(path);
+ if (info.coolType !== CooldownType.Unknown) {
+ config.push({
+ name: info.name,
+ label: "⬇️ " + info.label,
+ type: "checkbox"
+ });
+ count += 1;
+ } else {
+ log.warn("路径{0}未找到对应的刷新机制,跳过", path);
+ }
+ }
+ // 4. 写入新的配置(格式化输出)
+ file.writeTextSync(settingFile, JSON.stringify(config, null, 2));
+ log.info("共{0}组有效路线,请在脚本配置中勾选需要采集的材料", count);
+
+}
+
+
+// 采集选中的材料
+async function runGatherMode() {
+ const selectedMaterials = getSelectedMaterials();
+
+ if (selectedMaterials.length === 0) {
+ log.error("未选择任何材料,请在脚本配置中勾选所需项目");
+ return;
+ }
+ if (settings.stopAtTime) {
+ stopTime = calcStopTime(settings.stopAtTime);
+ log.info("脚本已被配置为达到{0}后停止运行", strftime(stopTime, true));
+ }
+
+ log.info("共{0}组材料路线待执行:", selectedMaterials.length);
+ for (const item of selectedMaterials) {
+ const info = getCooldownInfoFromPath(item.label);
+ log.info(` - {0} (${info.coolType})`, item.label || item.name);
+ }
+
+ let account = await getCurrentAccount();
+ log.info("为{0}采集材料并管理CD", account);
+
+ if (settings.partyName) {
+ try {
+ 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"));
+ // 可在此处继续处理 selectedMaterials 列表
+ try {
+ for (const pathTask of selectedMaterials) {
+ await runPathTaskIfCooldownExpired(account, pathTask);
+ }
+ } catch (e) {
+ if (e instanceof ReachStopTime) {
+ log.info("达到设置的停止时间 {0},终止运行", strftime(stopTime, true));
+ } else {
+ throw e;
+ }
+ }
+}
+
+
+// 清除运行记录(重置材料刷新时间)
+async function runClearMode() {
+ const selectedMaterials = getSelectedMaterials();
+
+ if (selectedMaterials.length === 0) {
+ log.error("未选择任何材料,请在脚本配置中勾选所需项目");
+ }
+ const resetTime = strftime(baseTime);
+ let account = await getCurrentAccount();
+ for (const pathTask of selectedMaterials) {
+ const jsonFiles = filterFilesInTaskDir(pathTask);
+ const recordFile = getRecordFilePath(account, pathTask);
+ const lines = jsonFiles.map((filePath) => {
+ return `${basename(filePath)}\t${resetTime}`;
+ });
+ const content = lines.join("\n");
+ file.writeTextSync(recordFile, content);
+ log.info("已重置{0}的刷新时间", pathTask.label);
+ }
+ log.info("已重置{0}组刷新时间。如需重置所有材料刷新时间,请直接删除record目录下对应账号的文件夹", selectedMaterials.length);
+}
+
+function getRecordFilePath(account, pathTask) {
+ const taskName = pathTask.name.replace(/^OPT_/, "");
+ return `record/${account}/${taskName}.txt`;
+}
+
+function filterFilesInTaskDir(pathTask, ext=".json") {
+ const taskDir = pathTask.label;
+ const allFilesRaw = file.ReadPathSync("pathing\\" + taskDir);
+ const extFiles = [];
+
+ for (const filePath of allFilesRaw) {
+ if (filePath.endsWith(ext)) {
+ extFiles.push(filePath);
+ }
+ }
+
+ return extFiles;
+}
+
+
+async function runPathTaskIfCooldownExpired(account, pathTask) {
+ const recordFile = getRecordFilePath(account, pathTask);
+ const jsonFiles = filterFilesInTaskDir(pathTask);
+
+ log.info("{0}共有{1}条路线", pathTask.label, jsonFiles.length);
+
+ // 2. 读取记录文件(路径 -> 时间)
+ const recordMap = {};
+ try {
+ const text = file.readTextSync(recordFile);
+ for (const line of text.split("\n")) {
+ const [p, t] = line.trim().split("\t");
+ if (p && t) {
+ recordMap[p] = new Date(t);
+ }
+ }
+ } catch (error) {
+ log.debug(`记录文件{0}不存在或格式错误`, recordFile);
+ }
+
+ // 3. 检查哪些 json 文件已过刷新时间
+ for (let i = 0; i < jsonFiles.length; i++) {
+ const jsonPath = jsonFiles[i];
+ const fileName = basename(jsonPath);
+ const lastTime = recordMap[fileName] || baseTime;
+ const pathName = fileName.split(".")[0];
+ const progress = `[${i + 1}/${jsonFiles.length}]`;
+
+ if (stopTime && Date.now() >= stopTime) {
+ throw new ReachStopTime("达到设置的停止时间,终止运行");
+ }
+
+ if (Date.now() > lastTime) {
+ let pathStart = addFakePathLog(fileName);
+ log.info(`${progress}{0}: 开始执行`, pathName);
+
+ let pathStartTime = new Date();
+ try {
+ await pathingScript.runFile(jsonPath);
+ } catch (error) {
+ log.error(`${progress}{0}: 文件不存在或执行失败: {1}`, pathName, error.toString());
+ addFakePathLog(fileName, pathStart);
+ continue; // 跳过当前任务
+ }
+
+ // 更新记录
+ if (new Date() - pathStartTime > 5000) {
+ recordMap[fileName] = calculateNextRunTime(new Date(), jsonPath);
+ const lines = [];
+
+ for (const [p, t] of Object.entries(recordMap)) {
+ lines.push(`${p}\t${strftime(t)}`);
+ }
+ const content = lines.join("\n");
+ file.writeTextSync(recordFile, content);
+ log.info(`${progress}{0}: 已完成,下次刷新: ${strftime(recordMap[fileName], true)}`, pathName);
+ } else {
+ log.info(`${progress}{0}: 执行时间过短,不更新记录`, pathName);
+ }
+ addFakePathLog(fileName, pathStart);
+ } else {
+ log.info(`${progress}{0}: 已跳过 (${strftime(recordMap[fileName], true)}刷新)`, pathName);
+ }
+ }
+}
+
+/**
+ * 根据路径逐级查找最匹配的物品,返回去除前缀的路径、标准化名称、刷新时间
+ * @param {string} fullPath - 单个完整路径(包含公共前缀)
+ * @returns {{ label: string, name: string, coolType: string }}
+ */
+function getCooldownInfoFromPath(fullPath) {
+ const parts = fullPath.split(/[\\/]/); // 支持 \ 或 / 分隔符
+ let cooldown = CooldownType.Unknown;
+ let cleanPart = "";
+
+ for (const part of parts) {
+ cleanPart = part.split("@")[0]; // 去除 @ 后缀
+
+ if (CooldownDataBase.hasOwnProperty(cleanPart)) {
+ cooldown = CooldownDataBase[cleanPart];
+ break;
+ }
+ }
+
+ const label = parts.slice(1).join("\\"); // 去除公共前缀
+ const name = "OPT_" + label.replace(/[^\u4e00-\u9fa5\w]/g, "_"); // 添加前缀并格式化名称
+
+ return {
+ label,
+ name,
+ coolType: cooldown,
+ };
+}
+
+function calculateNextRunTime(base, fullPath) {
+ const {coolType} = getCooldownInfoFromPath(fullPath);
+ let nextTime = baseTime;
+
+ 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;
+}
+
+
+/**
+ * 获取指定路径下所有最底层的文件夹(即不包含任何子文件夹的文件夹)
+ * @param {string} folderPath - 要遍历的根文件夹路径
+ * @param {string[]} result - 用于收集最底层文件夹路径的数组
+ * @returns {Promise} 所有最底层文件夹的路径
+ */
+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() {
+ const configText = file.readTextSync(settingFile);
+ const config = JSON.parse(configText); // 配置数组
+
+ const selectedMaterials = [];
+
+ for (const entry of config) {
+ if (
+ entry.name &&
+ entry.name.startsWith("OPT_") &&
+ entry.type === "checkbox"
+ ) {
+ if (settings[entry.name] === true) {
+ entry.label = entry.label.split(" ")[1]; // 去除⬇️指示
+ selectedMaterials.push(entry);
+ }
+ }
+ }
+
+ 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);
+}
diff --git a/repo/js/CD-Aware-AutoGather/manifest.json b/repo/js/CD-Aware-AutoGather/manifest.json
new file mode 100644
index 00000000..3dce6425
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/manifest.json
@@ -0,0 +1,15 @@
+{
+ "manifest_version": 1,
+ "name": "带CD管理的自动采集",
+ "version": "1.0",
+ "bgi_version": "0.45.0",
+ "description": "自动同步你通过BetterGI订阅的地图追踪任务,执行采集任务,并管理材料刷新时间(支持多账号)。\n首次运行前请先简单阅读说明,推荐在线版 https://github.com/babalae/bettergi-scripts-list/tree/main/repo/js/CD-Aware-AutoGather \n本地版说明见脚本目录内的 README.md 文件",
+ "authors": [
+ {
+ "name": "Ayaka-Main",
+ "link": "https://github.com/Patrick-Ze"
+ }
+ ],
+ "settings_ui": "settings.json",
+ "main": "main.js"
+}
\ No newline at end of file
diff --git a/repo/js/CD-Aware-AutoGather/preview.png b/repo/js/CD-Aware-AutoGather/preview.png
new file mode 100644
index 00000000..1a648cdc
Binary files /dev/null and b/repo/js/CD-Aware-AutoGather/preview.png differ
diff --git a/repo/js/CD-Aware-AutoGather/settings.json b/repo/js/CD-Aware-AutoGather/settings.json
new file mode 100644
index 00000000..ee15553b
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/settings.json
@@ -0,0 +1,10 @@
+[
+ {
+ "name": "runMode",
+ "type": "select",
+ "label": "首次运行前请先简单阅读说明,推荐在线版\n https://github.com/babalae/bettergi-scripts-list\n/tree/main/repo/js/CD-Aware-AutoGather \n本地版说明见脚本目录内的 README.md 文件",
+ "options": [
+ "扫描文件夹更新可选材料列表"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/repo/js/CD-Aware-AutoGather/settings.template.json b/repo/js/CD-Aware-AutoGather/settings.template.json
new file mode 100644
index 00000000..5b6012f3
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/settings.template.json
@@ -0,0 +1,27 @@
+[
+ {
+ "name": "runMode",
+ "type": "select",
+ "label": "运行模式",
+ "options": [
+ "扫描文件夹更新可选材料列表",
+ "采集选中的材料",
+ "清除运行记录(重置材料刷新时间)"
+ ]
+ },
+ {
+ "name": "partyName",
+ "type": "input-text",
+ "label": "设置要使用的队伍名称(留空则不进行切换)"
+ },
+ {
+ "name": "stopAtTime",
+ "type": "input-text",
+ "label": "停止运行时间(HH:mm,24小时制。例如09:28为上午9点28分)"
+ },
+ {
+ "name": "iHaveMultipleAccounts",
+ "type": "checkbox",
+ "label": "我肝的账号不止一个(选中后将分账号维护对应的材料刷新时间)"
+ }
+]
\ No newline at end of file
diff --git a/repo/js/CD-Aware-AutoGather/脚本思路.txt b/repo/js/CD-Aware-AutoGather/脚本思路.txt
new file mode 100644
index 00000000..3b4bc18a
--- /dev/null
+++ b/repo/js/CD-Aware-AutoGather/脚本思路.txt
@@ -0,0 +1,44 @@
+提供一个bat文件
+1. 创建符号链接,指向路径追踪文件夹
+
+js脚本逻辑:
+1. 设置文件完全由js脚本生成(可以提交一个生成版本进库作为参考)
+2. 提供3种运行模式
+ a 扫描文件夹更新可用配置
+ b 采集选中的材料
+ c 清除运行记录(重置刷新时间)
+
+可行性探索:
+1. 当配置选项新增可选物品时:获取该项目的值会得到undefined(等同于未配置)
+2. 当已选中物品从配置选项中删除时:获取该项目的值会得到undefined,但会保存在脚本组的配置里。如果再加回这个选项,会获得之前设置的值。不存在于当前settings.json文件的字段不会自动从脚本组配置中删除
+3. 设置属性里不支持特殊字符
+多账户支持:单账户时记录保存到`默认账号`文件夹,多账户时根据UID创建对应的记录文件夹
+
+索引文件列属性:
+只记录采集物名称以及对应CD,不记录完整路径。这样的话只要新的路径追踪是符合文件夹结构的,也能自动支持。
+工作时,基于文件夹路径,从前到后全词匹配路径的每个部分,直到找到对应的项目
+
+a 扫描模式
+1. 提示可以运行bat脚本或者手动创建符号链接
+2. 遍历追踪文件夹内的所有子文件夹
+3. 基于子文件夹的相对路径,查找索引文件,对于那些在索引中的条目:更新settings.json,创建 options 可选列表,并记录所有选项
+4. settings.json中还需要提供的配置项:
+ - 运行模式
+ - 队伍名称(如果不同的采集物需要使用不同队伍,那要求用户重复添加多个JS运行项)
+ - 终止运行时间
+ - 要采集的物品列表
+ - 我肝的账号不止一个
+5. 如果用户添加的文件夹太多,设置项也会很多。可以建议用户适当删除一些
+
+b 采集模式:
+1. 根据用户选中的采集物,枚举对应文件夹下的路径追踪文件。逐个处理完所有选项
+2. 根据子文件夹的相对路径,查找索引文件,得知其刷新模式
+3. 对于每个追踪文件,循环执行:
+ a. 查询运行记录,获知刷新时间。如果查询不到,视为未运行过
+ b. 如果当前时间大于刷新时间,则执行采集
+ c. 执行采集后,计算下次刷新时间并更新运行记录
+
+c 清除模式
+1. 根据用户选中的采集物,获取其对应的运行记录
+2. 重置运行记录中的刷新时间为绫华生日
+
diff --git a/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json b/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json
index a0ddb458..f5e5f15f 100644
--- a/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json
+++ b/repo/pathing/地方特产/枫丹/柔灯铃/柔灯铃——8——9个.json
@@ -25,8 +25,8 @@
"x": 3701.55,
"y": 3836.57,
"type": "target",
- "move_mode": "walk",
- "action": ""
+ "move_mode": "fly",
+ "action": "stop_flying"
},
{
"x": 3725.26,