diff --git a/repo/js/FullyAutoAndSemiAutoTools/README.md b/repo/js/FullyAutoAndSemiAutoTools/README.md
new file mode 100644
index 000000000..6c3e85ca4
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/README.md
@@ -0,0 +1,405 @@
+# FullyAutoAndSemiAutoTools
+
+**全自动 + 半自动路径任务工具箱**
+
+## 项目概述
+
+FullyAutoAndSemiAutoTools 是一套高度模块化、可高度自定义的自动化路径执行工具箱。
+
+支持**全自动**与**半自动**两种运行模式,核心目标包括:
+
+- 智能扫描 & 多层级管理大量路径文件(支持任意深度目录结构)
+- 精细的冷却时间控制(小时制 + Cron,需要外部解析服务)`根据路径的CD`
+- 根据路径特性自动切换战斗/元素队伍
+- 集成丰富的实时辅助功能
+- 执行记录防重复 + 择优优先未完成路径
+- 支持黑白名单、多 UID 隔离记录与路径缓存
+## 快捷使用
+- 1.(首次/刷新存储)账号刷新 设置刷新层级 运行一次 (自动识别uid存储) 先获取版本密钥
+
+ 
+- 2.执行
+ **选择 全自动**
+ 
+ **CD算法需要部署 [bettergi-scripts-tools](https://github.com/Kirito520Asuna/bettergi-scripts-tools/releases) 并启用http
+ 也可以开启择优模式(择优模式不依赖bettergi-scripts-tools)**
+ 
+ 
+ **日常选择 全自动**
+ 
+ **配置队伍**
+ 配置默认 行走队
+ 队伍配置 实例语法对应映射: 矿物,火,水,风,雷,草,冰,岩
+ 队伍配置 实例语法: 矿采集,水火风雷,水火风雷,水火风雷,元素,矿采集
+ 没有可留空 如 矿采集,,,,,矿采集
+ **锄地队伍配置**
+ 实例语法: 敌人与魔物->发条机关=速通,..... (还不清楚的看`锄地队伍配置图映射`)
+ 
+ **`锄地队伍配置图映射`**
+ 
+ **`选中执行组`**
+ 多选项 为或关系
+ 如选中 地方特产的璃月 和 璃月的石珀 会执行 地方特产的璃月下所有的路径
+ 如需要指定 请勿选中上级目录 璃月的石珀(3级路径)->地方特产的璃月(2级路径)
+ 如图选中下3级路径时不要选中对应的2级路径
+ 
+ 
+- 3.高级配置请继续查看文档后续
+### 核心亮点(最新版)
+
+- **路径扫描全面重构**:支持任意层级目录,父子关系清晰,性能稳定
+- **按 UID 缓存路径列表**(`path-json-by-uid.json`),多账号切换极快
+- 小时制 & Cron 双冷却系统(需搭配 bettergi-scripts-tools)
+- 智能队伍切换(战斗/七元素)
+- 实时任务:自动对话跳过、自动拾取、自动战斗
+- 择优执行 + 执行记录 + 错误路径追踪
+- 半自动快捷键干预(继续/跳过)
+- 开发者模式 + 详细日志 + UID自动识别
+
+## 文件结构一览
+
+```
+FullyAutoAndSemiAutoTools/
+├── config/
+│ ├── SevenElement.json # 七元素路径→队伍映射
+│ ├── cd-pathing.json # 路径冷却规则(hours/cron)
+│ ├── record.json # 本次执行记录
+│ ├── PathRecord.json # 历史路径执行记录
+│ ├── uidSettings.json # 用户配置快照(多UID支持)
+│ ├── path-json-by-uid.json # 按UID缓存的路径JSON列表
+│ └── RefreshSettings.json # 刷新配置缓存(可选)
+├── utils/
+│ ├── SwitchTeam.js # 队伍切换核心
+│ ├── cron.js # Cron 解析工具
+│ └── uid.js # UID 识别模块
+├── pathing/ # ★ 所有路径文件目录(需符号链接)
+├── SymLink.bat # 一键创建 pathing 链接
+├── main.js # 主程序入口(已重构路径扫描)
+├── manifest.json # 脚本元信息 & 版本标识
+└── settings.json # 配置模板(用户界面依据)
+```
+
+## 核心流程图
+
+### 1. 整体流程图(graph TD 风格,适合看结构与分支)
+
+```mermaid
+graph TD
+ A[启动脚本] --> B[读取 config_run]
+
+ B --> C{config_run ?}
+
+%% ────────────── 刷新模式 ──────────────
+ C -->|刷新| Refresh[刷新模式]
+
+ Refresh --> Scan[扫描 pathing 目录
readPaths 递归读取所有文件/文件夹]
+
+ Scan --> Depth[应用 loading_level
限制最大递归深度]
+
+ Depth --> BlackWhite[应用黑白名单过滤
config_white_list / config_black_list
(优先级高于其他过滤)]
+
+BlackWhite --> Generate[生成 multi-checkbox 配置项
写入 uidSettings.json
写入 path-json-by-uid.json
(此时黑白名单已生效)]
+
+Generate --> RefreshEnd[刷新结束
黑白名单已固化到缓存中]
+
+%% ────────────── 加载模式 ──────────────
+C -->|加载| Load[加载模式]
+
+Load --> ReadCache[读取上次缓存
path-json-by-uid.json + uidSettings.json
(已包含刷新时的黑白名单过滤结果)]
+
+ReadCache --> LoadFilter{二次过滤项}
+
+LoadFilter --> TheLayer[the_layer = true ?
只保留指定层级]
+
+LoadFilter --> HighFilter[high_level_filtering 有值 ?
路径字符串匹配过滤]
+
+LoadFilter --> BlackWhiteLoad[黑白名单输入框显示
但修改无效
(不重新过滤缓存)]
+
+TheLayer & HighFilter --> ShowOptions[显示最终过滤后的选项列表
供用户勾选]
+
+BlackWhiteLoad -.-> ShowOptions[黑白名单修改不生效]
+
+ShowOptions --> LoadEnd[加载结束
黑白名单不参与二次过滤]
+
+%% ────────────── 执行模式 ──────────────
+C -->|执行| Exec[执行模式
不显示黑白名单 / 层级 / 高阶过滤项
直接使用已勾选路径]
+
+Exec --> Run[读取 CD + 记录
构建 needRunMap → 执行]
+
+%% 样式
+classDef refresh fill:#e8f5e9,stroke:#2e7d32
+classDef load fill:#e3f2fd,stroke:#1976d2
+classDef exec fill:#f3e5f5,stroke:#7b1fa2
+classDef filter fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
+
+class Refresh,Scan,Depth,BlackWhite,Generate refresh
+class Load,ReadCache,TheLayer,HighFilter,BlackWhiteLoad,ShowOptions load
+class Exec,Run exec
+class BlackWhite,TheLayer,HighFilter filter
+```
+
+### 2. 执行时序图(sequenceDiagram 风格,适合看交互顺序)
+
+```mermaid
+sequenceDiagram
+ participant U as 用户
+ participant UI as 设置界面
+ participant S as 扫描 & 缓存模块
+ participant F as 过滤逻辑
+ participant R as 记录/缓存文件
(uidSettings.json + path-json-by-uid.json)
+ participant CD as 冷却检查
+ participant E as 执行引擎
+ participant T as 队伍切换
+ participant P as 路径执行
+
+ U->>UI: 选择 config_run 并保存
+
+ alt config_run = 刷新
+ UI->>S: 开始刷新(重新扫描 pathing 目录)
+ S->>F: 读取 loading_level
+ F-->>S: 限制递归深度
+ S->>F: 读取黑白名单
+ F-->>S: 过滤掉黑名单文件夹,保留白名单优先
+ Note over S,F: the_layer 与 high_level_filtering
在本模式不参与过滤
+ S->>R: 写入完整过滤后的路径列表 & 配置快照
+ R-->>UI: 刷新完成,显示新生成的选项列表
+ else config_run = 加载
+ UI->>R: 请求读取上次缓存
+ R-->>UI: 返回已缓存的路径列表
(已包含刷新时的黑白名单结果)
+ UI->>F: 显示 the_layer / high_level_filtering / loading_level 输入框
+ Note over UI,F: 黑白名单输入框显示,但修改无效
+ alt 用户修改了 the_layer 或 high_level_filtering
+ F->>F: 对缓存列表进行二次过滤
+ F-->>UI: 显示过滤后的选项列表
+ else 未修改或修改了黑白名单
+ F-->>UI: 沿用原缓存列表
(黑白名单修改被忽略)
+ end
+ UI-->>U: 显示可勾选的路径组
+ else config_run = 执行
+ UI->>R: 读取用户已勾选的路径组 & 配置
+ R-->>UI: 返回最终待执行列表
+ Note over UI,R: 此时所有层级/过滤/黑白名单配置已固化
不再受任何影响
+ UI->>CD: 检查冷却规则(hours / cron)
+ CD-->>UI: 过滤掉仍在CD的路径组
+ UI->>E: 开始执行(needRunMap)
+ loop 遍历每个路径组
+ E->>T: 根据路径特性决定队伍
+ alt 需要战斗 / 元素 / 锄地特化
+ T->>T: switchTeamByName / switchTeamByIndex
+ end
+ T-->>E: 队伍就位
+ alt 半自动模式
+ E->>U: 等待快捷键(继续/跳过)
+ U-->>E: 按键响应
+ end
+ E->>P: pathingScript.runFile(路径)
+ P-->>E: 执行完成 / 异常
+ E->>R: 记录执行结果(成功/失败/时间戳)
+ end
+ R-->>UI: 保存最终记录
+ UI-->>U: 执行完成
+ end
+```
+
+## 重要配置项一览(settings.json)
+
+| 配置项 | 类型 | 主要作用 | 推荐默认/示例 |
+|---------------------|-----|-------------------------------------------------------------------|----------------------------------------------|
+| config_uid | 复选框 | 当前配置uid:{无}(仅仅显示配置uid无其他作用) | xxx |
+| key | 文本 | 脚本密钥 | xxx |
+| config_run | 下拉 | 运行模式:刷新 / 加载 / 执行 | 刷新 → 首次,加载 → 日常 |
+| refresh_record | 复选框 | 配置模式-刷新-清空运行记录 | |
+| refresh_record_mode | 下拉 | 清空运行记录模式 全部 | UID | |
+| high_level_filtering | 文本 | 高阶过滤 | |
+| loading_level | 文本 | 路径层级深度(≥1,实际支持更高深度) | 2 或 3 |
+| the_layer | 复选框 | 只加载指定层级 | |
+| order_rules | 文本 | 执行顺序规则(可留空)
语法:父文件夹名称->文件夹名称=顺序整数,...
示例:食材与炼金->晶蝶=1, | ""(默认按扫描顺序)或 "食材与炼金->晶蝶=1,pathing->地方特产=2" |
+| open_limit_max | 复选框 | 开启执行组最大路径数配置 | |
+| limit_max_group | 文本 | 配置执行组最大路径数(可留空)
语法:父文件夹名称->文件夹名称=最大路径数,...
示例:食材与炼金->晶蝶=50, | ""(默认按扫描顺序)或 "食材与炼金->晶蝶=50,pathing->地方特产=50" |
+| config_white_list | 文本 | 白名单(逗号分隔) | 晶蝶,特产 |
+| config_black_list | 文本 | 黑名单(优先级更高) | 其他,锄地专区,周本 |
+| open_cd | 复选框 | 启用冷却控制 | 建议开启 |
+| http_api | 文本 | Cron 解析服务地址 | http://127.0.0.1:8081/... |
+| real_time_missions | 多选 | 实时辅助(对话/战斗/拾取) | 至少开「自动拾取」 |
+| choose_best | 复选框 | 择优模式(优先未跑/最久未跑路径) | 推荐开启 |
+| mode | 下拉 | 全自动 / 半自动 | 全自动(日常) |
+| auto_semi_key_mode | 下拉 | 半自动快捷键行为(继续/跳过) | 继续运行 |
+| auto_key | 文本 | 半自动干预快捷键 | F10 / F11(避免冲突) |
+| team_fight | 文本 | 通用行走队伍名称 | 必须填写 |
+| team_hoe_ground | 文本 | 锄地特化队伍配置(语法:父文件夹->子文件夹=队伍名,...) | 敌人与魔物->蕈兽=蕈兽队 |
+| team_seven_elements | 文本 | 七元素队伍(矿物,火,水,风,雷,草,冰,岩) | 按顺序填写 |
+| is_debug | 复选框 | 开发者模式(详细日志) | 调试时开启 |
+
+
+
+## 语法说明
+- high_level_filtering 高阶过滤
+```text
+语法:xxx->xxx-> ..... 无限制
+
+实例:pathing\地方特产\
+语法:地方特产
+实例:pathing\地方特产\枫丹\
+语法:地方特产->枫丹
+实例:pathing\地方特产\枫丹\幽光星星\
+语法:地方特产->枫丹->幽光星星
+实例:pathing\地方特产\枫丹\幽光星星\幽光星星@jbcaaa\
+语法:地方特产->枫丹->幽光星星->幽光星星@jbcaaa
+```
+- 以下语法风格请保持一致 ,否则会导致路径会出现异常运行 如使用 地产->nam=1 同时保持 地产->nam=队伍名1 则路径会出现异常运行
+- order_rules 执行顺序规则
+ 1. `rootName` 根目录下层文件夹名称,`parentName` 父目录名称,`name` 文件夹名称
+ 2. 建议语法:`rootName->parentName->name=1,rootName->parentName->name2=2`
+ 3. `rootName=parentName`时 语法`rootName->parentName->name=1`不可用, 请使用 `parentName->name=1` 语法
+ 4. `rootName->name=1` 语法不支持
+ 5. 匹配精度:`rootName->parentName->name` > `parentName->name` > 默认顺序
+- team_hoe_ground 锄地特化队伍配置
+ 1. `rootName` 根目录下层文件夹名称,`parentName` 父目录名称,`name` 文件夹名称
+ 2. 建议语法:`rootName->parentName->name=队伍名,rootName->parentName->name2=队伍名`
+ 3. `rootName=parentName`时 语法`rootName->parentName->name=队伍名`不可用, 请使用 `parentName->name=队伍名` 语法
+ 4. `rootName->name=队伍名` 语法不支持
+ 5. 匹配精度:`rootName->parentName->name` > `parentName->name` > 默认顺序
+- limit_max_group 配置执行组最大路径数
+ 1. `parentName` 父目录名称,`name` 文件夹名称
+ 2. 建议语法:`parentName->name=50,parentName->name2=50`
+ 3. 匹配精度: `parentName->name` > 默认顺序
+## (可选)额外json配置
+
+### 配置项order_rules
+
+路径:config/PathOrder.json
+order值越大优先级越高 `公共配置 锄地模块排序未配置 希望好心人完善 预留order 900~700`
+```json
+[
+ {
+ "uid": "",
+ //账号UID
+ "is_common": false,
+ //是否为公共配置
+ "root_name": "",
+ //根文件夹下对应的文件夹名称
+ "parent_name": "",
+ //父文件夹名称
+ "name": "",
+ //文件夹名称
+ "order": 0
+ //顺序
+ }
+]
+```
+
+### 配置项team_hoe_ground
+
+路径:config/HoeGround.json
+
+```json
+[
+ {
+ "uid": "",
+ //账号UID
+ "is_common": false,
+ //是否为公共配置
+ "root_name": "",
+ //根文件夹下对应的文件夹名称
+ "parent_name": "",
+ //父文件夹名称
+ "name": "",
+ //文件夹名称
+ "team_name": ""
+ //队伍名称
+ }
+]
+```
+
+## CD 规则示例(cd-pathing.json)
+level值越大优先级越高
+```json
+[
+ {
+ "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 * ?"
+ }
+]
+```
+## 运行实例部分日志
+实例==>运行矿物 大类 虹滴晶 运行完成 再次运行 日志截图展示:
+
+## Cron 解析服务部署(必须)
+
+**bettergi-scripts-tools** 是 Cron 解析的必要依赖,请至少选择一种方式部署:
+> bettergi-scripts-tools 版本需要 v0.0.3
+### 1. Windows 一键运行
+
+下载 [release](https://github.com/Kirito520Asuna/bettergi-scripts-tools/releases) 中的 windows zip 包 → 解压 → 双击 .exe
+运行
+
+### 2. Java 运行
+
+下载 jar 包 → 执行:
+
+```bash
+java -jar bettergi-scripts-tools-xxxx.jar
+```
+
+### 3. Docker 部署
+
+```bash
+docker pull ghcr.io/kirito520asuna/bettergi-scripts-tools:latest
+docker run -d -p 8081:8081 --name bettergi-scripts-tools ghcr.io/kirito520asuna/bettergi-scripts-tools:latest
+```
+
+**默认 API 地址**:`http://127.0.0.1:8081/bgi/cron/next-timestamp/all`
+
+## 推荐使用流程
+
+1. 双击 `SymLink.bat` 创建 pathing 链接
+2. 首次运行强烈建议先「刷新」一次(生成 path-json-by-uid.json 缓存)
+3. 日常使用「加载」模式 → 快速恢复配置 & 路径列表
+4. 微调路径组 → 切换「执行」模式 → 一键启动
+
+**多账号特别友好**:每个 UID 都有独立路径缓存,切换账号无需重复扫描
+
+## 小技巧与实用建议
+
+- 最省事组合:全自动 + 择优模式 + 自动拾取 + 冷却控制
+- 最安全组合:半自动 + 自动拾取 + 详细日志 + 大量黑名单
+- 路径目录再深也没关系,扫描已支持任意层级
+- 错误路径反复出现?查看 record.json 加黑名单或修复
+- 多UID用户:首次每个号都「刷新」一次,建立独立缓存
+
+## 注意事项
+
+- 首次使用或升级后建议删除旧 `path-json-by-uid.json` 重新生成
+- 半自动模式请确保快捷键不与 BetterGI 冲突
+- Cron 模式必须部署解析服务,否则冷却检查无效
+- 最低兼容版本:BetterGI 0.54.3+
+
+## 版本密钥
+
+| 版本 | 密钥 |
+|-------|-------------|
+| 0.0.1 | PGCSBY37NJA |
+
+## 版本历史(简要)
+
+### 0.0.1 2026.01.30
+
+基本功能完成
+
+**作者**:云端客 (Kirito520Asuna)
+
+祝你使用愉快,素材永远收不完~✨
diff --git a/repo/js/FullyAutoAndSemiAutoTools/SymLink.bat b/repo/js/FullyAutoAndSemiAutoTools/SymLink.bat
new file mode 100644
index 000000000..e6cc5f6df
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/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/FullyAutoAndSemiAutoTools/assets/Configure Team Button.png b/repo/js/FullyAutoAndSemiAutoTools/assets/Configure Team Button.png
new file mode 100644
index 000000000..e6bdd02af
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/Configure Team Button.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/assets/Confirm Deploy Button.png b/repo/js/FullyAutoAndSemiAutoTools/assets/Confirm Deploy Button.png
new file mode 100644
index 000000000..6c870dfc6
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/Confirm Deploy Button.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/assets/Leave Button.png b/repo/js/FullyAutoAndSemiAutoTools/assets/Leave Button.png
new file mode 100644
index 000000000..58c150b41
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/Leave Button.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/assets/Quick Setup Button.png b/repo/js/FullyAutoAndSemiAutoTools/assets/Quick Setup Button.png
new file mode 100644
index 000000000..4b7e37512
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/Quick Setup Button.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/assets/Slider Bottom.png b/repo/js/FullyAutoAndSemiAutoTools/assets/Slider Bottom.png
new file mode 100644
index 000000000..4af2e3cb2
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/Slider Bottom.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/assets/Slider Top.png b/repo/js/FullyAutoAndSemiAutoTools/assets/Slider Top.png
new file mode 100644
index 000000000..7334e462d
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/Slider Top.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/assets/paimon_menu.png b/repo/js/FullyAutoAndSemiAutoTools/assets/paimon_menu.png
new file mode 100644
index 000000000..c424325b1
Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/assets/paimon_menu.png differ
diff --git a/repo/js/FullyAutoAndSemiAutoTools/config/LimitMax.json b/repo/js/FullyAutoAndSemiAutoTools/config/LimitMax.json
new file mode 100644
index 000000000..b323fc8e6
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/config/LimitMax.json
@@ -0,0 +1,114 @@
+[
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "虹滴晶",
+ "max": 50
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "萃凝晶",
+ "max": 60
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "紫晶块",
+ "max": 50
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "水晶块",
+ "max": 50
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "紫晶块",
+ "name": "紫晶块[大剑]@蜜柑魚",
+ "max": 25
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "紫晶块",
+ "name": "紫晶块[钟离版]@mfkvfhpdx",
+ "max": 25
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "萃凝晶",
+ "name": "萃凝晶-火神赶路-大剑@无限不循环",
+ "max": 30
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "萃凝晶",
+ "name": "纳塔[钟离版]@成成",
+ "max": 10
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "萃凝晶",
+ "name": "枫丹水下@芝士贝果",
+ "max": 10
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "萃凝晶",
+ "name": "枫丹[钟离版]@cy",
+ "max": 10
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "水晶块",
+ "name": "水晶块@忆雪晴",
+ "max": 20
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "水晶块",
+ "name": "水晶块[大剑]@火山@芝士贝果",
+ "max": 20
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "水晶块",
+ "name": "水晶块[钟离版]@愚溪",
+ "max": 10
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "",
+ "parent_name": "白铁块",
+ "name": "富集路线@Tool_tingsu",
+ "max": 15
+ }
+]
\ No newline at end of file
diff --git a/repo/js/FullyAutoAndSemiAutoTools/config/PathOrder.json b/repo/js/FullyAutoAndSemiAutoTools/config/PathOrder.json
new file mode 100644
index 000000000..35b19998d
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/config/PathOrder.json
@@ -0,0 +1,106 @@
+[
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "挪德卡莱",
+ "order": 980
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "纳塔",
+ "order": 979
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "枫丹",
+ "order": 978
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "须弥",
+ "order": 977
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "稻妻",
+ "order": 976
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "璃月",
+ "order": 975
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "地方特产",
+ "parent_name": "地方特产",
+ "name": "蒙德",
+ "order": 974
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "虹滴晶",
+ "order": 940
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "萃凝晶",
+ "order": 939
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "紫晶块",
+ "order": 938
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "水晶块",
+ "order": 937
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "星银矿石",
+ "order": 936
+ },
+ {
+ "uid": "",
+ "is_common": true,
+ "root_name": "矿物",
+ "parent_name": "矿物",
+ "name": "白铁块",
+ "order": 935
+ }
+]
\ No newline at end of file
diff --git a/repo/js/FullyAutoAndSemiAutoTools/config/SevenElement.json b/repo/js/FullyAutoAndSemiAutoTools/config/SevenElement.json
new file mode 100644
index 000000000..2963e7d15
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/config/SevenElement.json
@@ -0,0 +1,42 @@
+[
+{
+ "name": "矿物",
+ "level": 0,
+ "value": ["夜泊石", "石珀", "清水玉", "万相石", "矿物"]
+},
+{
+ "name": "火",
+ "level": 1,
+ "value": []
+},
+{
+ "name": "水",
+ "level": 2,
+ "value": ["海露花"]
+},
+{
+ "name": "风",
+ "level": 3,
+ "value": ["蒲公英籽"]
+},
+{
+ "name": "雷",
+ "level": 4,
+ "value": ["琉鳞石", "绯樱绣球"]
+},
+{
+ "name": "草",
+ "level": 5,
+ "value": []
+},
+{
+ "name": "冰",
+ "level": 6,
+ "value": []
+},
+{
+ "name": "岩",
+ "level": 7,
+ "value": []
+}
+]
\ No newline at end of file
diff --git a/repo/js/FullyAutoAndSemiAutoTools/config/cd-pathing.json b/repo/js/FullyAutoAndSemiAutoTools/config/cd-pathing.json
new file mode 100644
index 000000000..3d4c74d9a
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/config/cd-pathing.json
@@ -0,0 +1,134 @@
+[
+ {
+ "name": "地方特产",
+ "type": "hours",
+ "level": 1,
+ "value": 46
+ },
+ {
+ "name": "星银矿石",
+ "type": "cron",
+ "level": 2,
+ "value": "0 0 0 1/2 * ?"
+ },
+ {
+ "name": "白铁块",
+ "type": "cron",
+ "level": 2,
+ "value": "0 0 0 1/2 * ?"
+ },
+ {
+ "name": "矿物",
+ "type": "cron",
+ "level": 1,
+ "value": "0 0 0 1/3 * ?"
+ },
+ {
+ "name": "晶蝶",
+ "type": "hours",
+ "level": 2,
+ "value": 12
+ },
+ {
+ "name": "「冷鲜肉」",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "发光髓",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "鳅鳅宝玉",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "青蛙",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "鳗肉",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "螃蟹",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "禽肉",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "蜥蜴尾巴",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "兽肉",
+ "type": "hours",
+ "level": 2,
+ "value": "12"
+ },
+ {
+ "name": "沉玉仙茗",
+ "type": "hours",
+ "level": 2,
+ "value": "24"
+ },
+ {
+ "name": "冰雾花花朵",
+ "type": "hours",
+ "level": 2,
+ "value": "46"
+ },
+ {
+ "name": "奇异的「牙齿」",
+ "type": "hours",
+ "level": 2,
+ "value": "46"
+ },
+ {
+ "name": "烈焰花花蕊",
+ "type": "hours",
+ "level": 2,
+ "value": "46"
+ },
+ {
+ "name": "电气水晶",
+ "type": "cron",
+ "level": 2,
+ "value": "0 0 0 1/2 * ?"
+ },
+ {
+ "name": "食材与炼金",
+ "type": "cron",
+ "level": 1,
+ "value": "0 0 0 1/1 * ?"
+ },
+ {
+ "name": "敌人与魔物",
+ "type": "cron",
+ "level": 1,
+ "value": "0 0 4 1/1 * ?"
+ },
+ {
+ "name": "锄地专区",
+ "type": "cron",
+ "level": 1,
+ "value": "0 0 4 1/1 * ?"
+ }
+]
\ No newline at end of file
diff --git a/repo/js/FullyAutoAndSemiAutoTools/config/settings.json b/repo/js/FullyAutoAndSemiAutoTools/config/settings.json
new file mode 100644
index 000000000..734227c89
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/config/settings.json
@@ -0,0 +1,199 @@
+[
+ {
+ "name": "config_uid",
+ "type": "checkbox",
+ "label": "当前配置uid:{无}\n(仅仅显示配置uid无其他作用)",
+ "default": false
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "key",
+ "type": "input-text",
+ "label": "密钥",
+ "default": ""
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "config_run",
+ "type": "select",
+ "label": "配置模式",
+ "options": [
+ "刷新",
+ "加载",
+ "执行"
+ ],
+ "default": "刷新"
+ },
+ {
+ "name": "refresh_record",
+ "type": "checkbox",
+ "label": "配置模式-刷新-清空运行记录",
+ "default": false
+ },
+ {
+ "name": "refresh_record_mode",
+ "type": "select",
+ "label": "清空运行记录模式",
+ "options": [
+ "全部",
+ "UID"
+ ],
+ "default": "UID"
+ },
+ {
+ "name": "loading_level",
+ "type": "input-text",
+ "label": "加载路径层级(不可小于1)<配置模式 刷新||加载 生效>",
+ "default": "2"
+ },
+ {
+ "name": "the_layer",
+ "type": "checkbox",
+ "label": "只加载指定层级<配置模式 加载 生效>",
+ "default": false
+ },
+ {
+ "name": "high_level_filtering",
+ "type": "input-text",
+ "label": "高阶过滤{语法看文档}<配置模式 加载 生效>",
+ "default": ""
+ },
+ {
+ "name": "order_rules",
+ "type": "input-text",
+ "label": "执行顺序规则(可留空)\n{语法:父文件夹名称->文件夹名称=顺序请填整数,.....}",
+ "default": ""
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "config_white_list",
+ "type": "input-text",
+ "label": "刷新白名单 以,分割",
+ "default": "晶蝶"
+ },
+ {
+ "name": "config_black_list",
+ "type": "input-text",
+ "label": "刷新黑名单 以,分割",
+ "default": "其他,锄地专区,食材与炼金"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "open_cd",
+ "type": "checkbox",
+ "label": "启用CD算法",
+ "default": false
+ },
+ {
+ "name": "http_api",
+ "type": "input-text",
+ "label": "[默认CD算法api]\ncron解析Http地址\n[请部署bettergi-scripts-tools可支持]",
+ "default": "http:///bgi/cron/next-timestamp/all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "real_time_missions",
+ "type": "multi-checkbox",
+ "label": "实时任务",
+ "options": [
+ "自动对话",
+ "自动战斗(已弃用)",
+ "自动拾取"
+ ],
+ "default": [
+ "自动拾取"
+ ]
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "choose_best",
+ "type": "checkbox",
+ "label": "择优模式(默认关闭 优先跑之前没跑过的)",
+ "default": false
+ },
+ {
+ "name": "mode",
+ "type": "select",
+ "label": "模式",
+ "options": [
+ "全自动",
+ "半自动"
+ ],
+ "default": "全自动"
+ },
+ {
+ "name": "auto_semi_key_mode",
+ "type": "select",
+ "label": "[半自动]快捷键模式",
+ "options": [
+ "继续运行",
+ "跳过"
+ ],
+ "default": "继续运行"
+ },
+ {
+ "name": "auto_key",
+ "type": "input-text",
+ "label": "<继续运行|跳过>本次路线快捷键 (独立BGI的快捷键请勿冲突)"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "open_limit_max",
+ "type": "checkbox",
+ "label": "开启执行组最大路径设置"
+ },
+ {
+ "name": "limit_max_group",
+ "type": "input-text",
+ "label": "配置执行组最大路径数\n{语法:父文件夹名称->文件夹名称=最大路径数,.....}\n如:敌人与魔物->蕈兽=50",
+ "default": ""
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "team_fight",
+ "type": "input-text",
+ "label": "战斗队伍配置(同时也是行走队伍配置)\n已禁用战斗检测,战斗队伍配置失效\n只留行走队伍配置"
+ },
+ {
+ "name": "team_hoe_ground",
+ "type": "input-text",
+ "label": "锄地队伍配置\n{语法:父文件夹名称->文件夹名称=队伍名称,.....}\n如:敌人与魔物->蕈兽=队伍名称",
+ "default": ""
+ },
+ {
+ "name": "team_seven_elements",
+ "type": "input-text",
+ "label": "队伍配置 按 `矿物,火,水,风,雷,草,冰,岩` 该顺序填写(建议草神请填至`草`位)",
+ "default": ""
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "name": "is_debug",
+ "type": "checkbox",
+ "label": "开发者模式",
+ "default": false
+ },
+ {
+ "name": "debug",
+ "type": "input-text",
+ "label": "调试快捷键(开发者)"
+ }
+]
\ No newline at end of file
diff --git a/repo/js/FullyAutoAndSemiAutoTools/main.js b/repo/js/FullyAutoAndSemiAutoTools/main.js
new file mode 100644
index 000000000..72ed59b45
--- /dev/null
+++ b/repo/js/FullyAutoAndSemiAutoTools/main.js
@@ -0,0 +1,2215 @@
+let manifest_json = "manifest.json";
+let manifest = undefined
+let configSettings = undefined
+const auto = {
+ semi: false,//半自动
+ run: false,//运行
+ skip: false,//跳过
+ key: "",
+}
+const dev = {
+ isDebug: false,
+ debug: undefined,
+}
+const cd = {
+ open: settings.open_cd || false,
+ http_api: settings.http_api || undefined,
+}
+const pathingName = "pathing"
+let loadingLevel = 2
+// const pathAsMap = new Map([])
+// const pathRunMap = new Map([])
+const needRunMap = new Map([])
+const PATHING_ALL = new Array({
+ id: `${pathingName}`,
+ level: 0,
+ name: `${pathingName}`,
+ parent_name: "",
+ child_names: []
+})
+let settingsNameList = new Array()
+const settingsNameAsList = new Array()
+let PATH_JSON_LIST = new Array()
+const config_root = 'config'
+const levelName = "treeLevel"
+const json_path_name = {
+ RecordText: `${config_root}\\record.json`,
+ RecordPathText: `${config_root}\\PathRecord.json`,
+ uidSettingsJson: `${config_root}\\uidSettings.json`,
+ templateMatchSettingsJson: `${config_root}\\settings.json`,
+ cdPath: `${config_root}\\cd-${pathingName}.json`,
+ SevenElement: `${config_root}\\SevenElement.json`,
+ RefreshSettings: `${config_root}\\RefreshSettings.json`,
+ pathJsonByUid: `${config_root}\\path-json-by-uid.json`,
+ PathOrder: `${config_root}\\PathOrder.json`,
+ HoeGround: `${config_root}\\HoeGround.json`,
+ LimitMax: `${config_root}\\LimitMax.json`,
+}
+// 定义记录文件的路径
+// let RecordText = `${config_root}\\record.json`
+// let RecordPathText = `${config_root}\\PathRecord.json`
+let RecordList = new Array()
+let RecordPathList = new Array()
+let RecordLast = {
+ uid: "",
+ data: undefined,
+ timestamp: 0,
+ paths: new Set(), // 记录路径
+ errorPaths: new Set(),
+ groupPaths: new Set(),
+}
+const Record = {
+ uid: "",
+ data: undefined,
+ timestamp: 0,
+ paths: new Set(), // 记录路径
+ errorPaths: new Set(), // 记录错误路径
+ groupPaths: new Set(), // 记录分组路径
+}
+let RecordPath = {
+ uid: "",
+ paths: new Set(), // 记录路径
+ //{timestamp,path}
+}
+const config_list = {
+ black: [],
+ white: [],
+}
+
+const SevenElement = {
+ SevenElements: ['矿物', '火', '水', '风', '雷', '草', '冰', '岩'],
+ SevenElementsMap: new Map([
+ ['矿物', ['夜泊石', '石珀', '清水玉', '万相石', '矿物']],
+ ['火', []],
+ ['水', ['海露花']],
+ ['风', ['蒲公英籽']],
+ ['雷', ['琉鳞石', '绯樱绣球']],
+ ['草', []],
+ ['冰', []],
+ ['岩', []],
+ ]),
+}
+
+
+const team = {
+ current: undefined,
+ fight: false,
+ fightName: settings.team_fight,
+ fightKeys: ['锄地专区', "敌人与魔物"],
+ SevenElements: settings.team_seven_elements ? settings.team_seven_elements.split(',').map(item => item.trim()) : [],
+ HoeGroundMap: new Map([]),
+
+}
+const timeType = Object.freeze({
+ hours: 'hours',//小时
+ cron: 'cron',//cron表达式
+ // 添加反向映射(可选)
+ fromValue(value) {
+ return Object.keys(this).find(key => this[key] === value);
+ }
+});
+
+/**
+ * 实时任务处理函数
+ * @param {boolean} is_common - 是否为通用任务标志
+ * @returns {void} 无返回值
+ */
+async function realTimeMissions(is_common = true) {
+ let real_time_missions = settings.real_time_missions // 从设置中获取实时任务列表
+ if (!Array.isArray(real_time_missions)) {
+ real_time_missions = [...real_time_missions]
+ }
+ if (!is_common) { // 处理非通用任务
+ if (real_time_missions.includes("自动战斗")) {
+ // await dispatcher.runAutoFightTask(new AutoFightParam());
+ await dispatcher.runTask(new SoloTask("AutoFight")); // 执行自动战斗任务
+ }
+ return // 非通用任务处理完毕后直接返回
+ }
+ // 处理通用任务
+ if (real_time_missions.includes("自动对话")) {
+ dispatcher.addTrigger(new RealtimeTimer("AutoSkip")); // 添加自动对话触发器
+ }
+ if (real_time_missions.includes("自动拾取")) {
+ // 启用自动拾取的实时任务
+ dispatcher.addTrigger(new RealtimeTimer("AutoPick")); // 添加自动拾取触发器
+ }
+
+}
+
+/**
+ * 根据用户ID加载路径JSON列表
+ * 该函数尝试从指定路径读取并解析JSON文件,将解析后的数据转换为Map对象,
+ * 然后根据当前用户ID获取对应的路径列表
+ *
+ * @returns {boolean} 加载成功返回true,失败返回false
+ */
+function loadPathJsonListByUid() {
+ try {
+ // 读取并解析JSON文件内容
+ const raw = JSON.parse(file.readTextSync(json_path_name.pathJsonByUid));
+ // 将解析后的数组转换为Map对象
+ const map = new Map(raw);
+
+ // 获取当前用户ID对应的路径列表
+ const list = map.get(Record.uid);
+ // 检查获取的列表是否为有效数组且不为空
+ if (Array.isArray(list) && list.length > 0) {
+ // 更新全局PATH_JSON_LIST变量
+ PATH_JSON_LIST = list;
+ // 记录成功日志,包含用户ID和路径数量
+ log.info(
+ "[PATH] 已加载 PATH_JSON_LIST,uid={0},count={1}",
+ Record.uid,
+ PATH_JSON_LIST.length
+ );
+ return true;
+ }
+ } catch (e) {
+ // 捕获并记录异常信息
+ log.warn("[PATH] 加载 PATH_JSON_LIST 失败: {0}", e.message);
+ }
+ return false;
+}
+
+async function initRefresh(settingsConfig) {
+ let level = 0
+ const parent_level = level + 1
+ // 获取当前路径下的所有文件/文件夹
+ let pathSyncList = file.readPathSync(`${PATHING_ALL[level].name}`);
+ log.debug("{0}文件夹下有{1}个文件/文件夹", `${pathingName}`, pathSyncList.length);
+ // 预处理黑白名单数组,移除空字符串并trim
+ const processedBlackList = config_list.black
+ .map(item => item.trim())
+ .filter(item => item !== "");
+
+ const processedWhiteList = config_list.white
+ .map(item => item.trim())
+ .filter(item => item !== "");
+ let blacklistSet = new Set(processedBlackList)
+ processedWhiteList.forEach(item => {
+ blacklistSet.delete(item)
+ })
+ const blacklist = Array.from(blacklistSet)
+
+ let settingsList = settingsConfig
+ let settingsRefreshList = []
+ let parentJson = {
+ name: `${levelName}_${level}_${level}`,
+ type: "multi-checkbox",
+ label: `选择要执行的${parent_level}级路径`,
+ options: []
+ }
+ for (const element of pathSyncList) {
+ // log.warn("element={0}", element)
+ const item = element.replace(`${pathingName}\\`, "");
+ if (!blacklist.find(black => item === black)) {
+ parentJson.options.push(item)
+ }
+ }
+ // settingsRefreshList.push({type: "separator"})
+
+ let treePathList = await readPaths(`${pathingName}`)
+ await debugKey('[init-refresh]_log-treePathList.json', JSON.stringify(treePathList))
+ let pathJsonList = await treeToList(treePathList)
+ await debugKey('[init-refresh]_log-pathJsonList.json', JSON.stringify(pathJsonList))
+ let obj = {
+ id: `${pathingName}`,
+ level: level,
+ name: `${pathingName}`,
+ parentId: undefined,
+ parentName: undefined,
+ rootName: undefined, // 最父级下一级名称
+ child_names: parentJson.options
+ };
+ await addUniquePath(obj)
+
+ PATH_JSON_LIST = pathJsonList
+
+ const forList = pathJsonList.filter(item => item.isFile)
+ for (const element of forList) {
+ const pathRun = element.path
+
+ // 检查路径是否被允许
+ const isBlacklisted = processedBlackList.some(item => pathRun?.includes(item));
+ const isWhitelisted = processedWhiteList.some(item => pathRun?.includes(item));
+
+ if (isBlacklisted && !isWhitelisted) {
+ continue;
+ }
+ //方案1
+ try {
+ loadingLevel = parseInt(settings.loading_level)
+ } catch (e) {
+ log.warn("配置 {0} 错误,将使用默认值{0}", "加载路径层级", loadingLevel)
+ } finally {
+ //
+ loadingLevel = loadingLevel < 1 ? 2 : loadingLevel
+ }
+ // 优化版本
+ for (let i = 0; i < loadingLevel; i++) {
+ const currentLevel = parent_level + 1 + i;
+ const parentLevel = parent_level + i;
+
+ const currentName = getChildFolderNameFromRoot(pathRun, currentLevel);
+ const childName = getChildFolderNameFromRoot(pathRun, currentLevel + 1);
+
+ // 检查当前层级是否存在
+ if (!currentName) {
+ break; // 没有当前层级,停止处理
+ }
+
+ // 过滤JSON文件
+ const filteredChildName = childName?.endsWith(".json") ? undefined : childName;
+ let child_names = Array.from(new Set(filteredChildName ? [filteredChildName] : []).difference(new Set(blacklist)))
+ // 获取父级名称用于建立层级关系
+ const parentName = getChildFolderNameFromRoot(pathRun, parentLevel);
+ const rootName = getChildFolderNameFromRoot(pathRun, parent_level + 1);
+
+ let obj1 = {
+ id: undefined,
+ parentId: element.parentId,
+ level: parentLevel, // 存储到目标层级 属于目标层级
+ name: currentName, // 当前层级名称
+ parentName: parentName, // 父级名称
+ rootName: rootName, // 最父级下一级名称
+ child_names: [...child_names]
+ };
+ await addUniquePath(obj1);
+ }
+ }
+ // 正确的排序方式
+ PATHING_ALL.sort((a, b) => {
+ // 首先按 level 排序
+ if (a.level !== b.level) {
+ return a.level - b.level;
+ }
+ const pathA = a?.path || '';
+ const pathB = b?.path || '';
+ // if (a.parent_name !== b.parent_name) {
+ // return a.parent_name.localeCompare(b.parent_name);
+ // }
+ // level 相同时按 path 排序
+ return pathA.localeCompare(pathB);
+ });
+
+ await debugKey('[init-refresh]_log-PATHING_ALL.json', JSON.stringify(PATHING_ALL))
+ const groupLevel = groupByLevel(PATHING_ALL);
+ await debugKey('[init-refresh]_log-groupLevel.json', JSON.stringify(groupLevel))
+ // const initLength = settingsList.length
+ let parentNameLast = undefined
+ // let parentNameNow = undefined
+ // const line = 30
+ // const br = `${"=".repeat(line)}\n`
+ let idx = 0
+ // const settingsSortMap = new Map([])
+ settingsRefreshList.push({type: "separator"})
+ settingsRefreshList.push({type: "separator"})
+ groupLevel.filter(list => list.length > 0).forEach(
+ (list) => {
+ let i = 0
+ list.filter(item => item && item.child_names && item.child_names.length > 0).forEach(item => {
+ const name = `${levelName}_${item.level}_${i}`
+ let prefix = ''
+ log.debug(`[{2}]Last{0},Current{1},Name{5}`, "比对", parentNameLast, item.parentName, item.name)
+ const isCommonLastAndCurrent = item.parentName !== parentNameLast;
+ if (isCommonLastAndCurrent) {
+ parentNameLast = item.parentName;
+ // let b = (line - item.parentName.length) % 2 === 0;
+ // const localLine = b ? ((line - item.parentName.length) / 2) : (Math.ceil((line - item.parentName.length) / 2))
+ // prefix = br + `${"=".repeat(localLine)}${item.parentName}${"=".repeat(localLine)}\n` + br
+ }
+ // const p = idx === 0 ? "【地图追踪】\n" : `${prefix}[${item.parent_name}-${item.name}]\n`
+ const p = `\n${prefix}${(item.rootName && item.name !== item.rootName) ? "《" + item.rootName + "》->" : ""}[${item.name}]`
+ idx++
+ let leveJson = {
+ name: `${name}`,
+ type: "multi-checkbox",
+ label: `选择要执行的${item.level + 1}级路径${p}`,
+ options: []
+ }
+ // todo: 待优化
+ const targetItem = PATH_JSON_LIST.filter(list_item => !list_item.levelName)
+ .find(list_item => list_item.id === item.parentId);
+ if (targetItem) {
+ targetItem.levelName = name || undefined;
+ }
+
+ // leveJson.options = leveJson.options.concat(item.child_names)
+ leveJson.options = [...item.child_names]
+ if (leveJson.options && leveJson.options.length > 0) {
+ settingsNameAsList.push({
+ settings_name: name,
+ settings_as_name: item.name
+ })
+ settingsNameList.push(name)
+ const existingIndex = settingsRefreshList.findIndex(item => item.name === leveJson.name);
+ if (existingIndex !== -1) {
+ // 替换已存在的配置项
+ settingsRefreshList[existingIndex] = leveJson;
+ } else {
+ if (isCommonLastAndCurrent) {
+ settingsRefreshList.push({type: "separator"})
+ }
+ parentNameLast = item.parentName;
+ settingsRefreshList.push({type: "separator"})
+ // 添加新的配置项
+ settingsRefreshList.push(leveJson);
+ }
+ i++
+ }
+ })
+ }
+ )
+ await debugKey('[init-refresh]_log-settingsRefreshList.json', JSON.stringify(settingsRefreshList))
+ //settingsRefreshList 二级排序 todo:
+ level++
+ settingsList = Array.from(new Set(settingsList.concat(settingsRefreshList)))
+
+ settingsList.filter(
+ item => item.name === 'key'
+ ).forEach(item => {
+ // 刷新settings自动设置密钥
+ item.default = manifest.key
+ })
+ settingsList.filter(
+ item => item.name === 'config_run'
+ ).forEach(item => {
+ // 刷新settings自动设置执行
+ item.default = "执行"
+ })
+ const uidSettingsMap = new Map([])
+ // 更新当前用户的配置
+ uidSettingsMap.set(Record.uid, settingsList)
+ // 安全写入配置文件
+ try {
+ file.writeTextSync(json_path_name.uidSettingsJson, JSON.stringify([...uidSettingsMap]))
+ log.debug("用户配置已保存: {uid}", Record.uid)
+ } catch (error) {
+ log.error("保存用户配置失败: {error}", error.message)
+ }
+ settingsList.filter(
+ item => item.name === 'config_uid'
+ ).forEach(item => {
+ // 刷新settings自动设置执行
+ item.label = "当前配置uid:\{" + Record.uid + "\}\n(仅仅显示配置uid无其他作用)"
+ })
+ file.writeTextSync(manifest.settings_ui, JSON.stringify(settingsList))
+
+ // ===== 保存 PATH_JSON_LIST(按 uid)=====
+ try {
+ let pathJsonMap = new Map();
+
+ try {
+ const raw = JSON.parse(file.readTextSync(json_path_name.pathJsonByUid));
+ pathJsonMap = new Map(raw);
+ } catch (e) {
+ log.debug("PATH_JSON_LIST 映射文件不存在,将新建");
+ }
+
+ pathJsonMap.set(Record.uid, PATH_JSON_LIST);
+
+ file.writeTextSync(
+ json_path_name.pathJsonByUid,
+ JSON.stringify([...pathJsonMap])
+ );
+
+ log.info(
+ "[PATH] 已保存 PATH_JSON_LIST,uid={0},count={1}",
+ Record.uid,
+ PATH_JSON_LIST.length
+ );
+ } catch (e) {
+ log.error("[PATH] 保存 PATH_JSON_LIST 失败: {0}", e.message);
+ }
+ if (settings.refresh_record) {
+ if (settings.refresh_record_mode === "UID") {
+ RecordList = RecordList.filter(item => item.uid !== Record.uid)
+ RecordPathList = RecordPathList.filter(item => item.uid !== Record.uid)
+ file.writeTextSync(json_path_name.RecordPathText, JSON.stringify(RecordPathList))
+ file.writeTextSync(json_path_name.RecordText, JSON.stringify(RecordList))
+ log.info("已清空UID:{0}记录文件", Record.uid)
+ return
+ }
+ file.writeTextSync(json_path_name.RecordPathText, JSON.stringify([]))
+ file.writeTextSync(json_path_name.RecordText, JSON.stringify([]))
+ log.info("已清空全部记录文件")
+ }
+}
+
+
+/**
+ * 初始化用户ID设置映射表
+ * @param {Map} uidSettingsMap - 用于存储用户ID设置的Map对象
+ * @returns {Map} 返回初始化后的用户ID设置映射表
+ */
+async function initUidSettingsMap(uidSettingsMap) {
+ // 获取用户设置JSON文件的路径
+ const uidSettingsJson = json_path_name.uidSettingsJson;
+ try {
+ // 读取并解析JSON文件内容,转换为Map对象
+ const existingData = JSON.parse(file.readTextSync(uidSettingsJson))
+ uidSettingsMap = new Map(existingData)
+ } catch (e) {
+ // 文件不存在时使用空Map
+ log.debug("配置文件不存在,将创建新的");
+ }
+ return uidSettingsMap;
+}
+
+/**
+ * 加载用户ID设置映射表
+ * @param {Map} uidSettingsMap - 用户ID到设置项的映射表
+ */
+async function loadUidSettingsMap(uidSettingsMap) {
+ // 从映射表中获取当前用户的设置
+ let uidSettings = uidSettingsMap.get(Record.uid);
+ // 如果存在用户设置
+ if (uidSettings) {
+ if (!loadPathJsonListByUid()) {
+ throw new Error(
+ "未找到 PATH_JSON_LIST,请先执行一次【刷新配置】"
+ );
+ }
+ try {
+ let templateMatchSettings = JSON.parse(file.readTextSync(json_path_name.templateMatchSettingsJson));
+ // 筛选出名称为'config_run'的设置项
+ templateMatchSettings.filter(
+ item => item.name === 'config_run'
+ ).forEach(item => {
+ // 刷新settings自动设置执行
+ item.default = "执行"
+ })
+ templateMatchSettings.filter(
+ item => item.name === 'config_uid'
+ ).forEach(item => {
+ // 刷新settings自动设置执行
+ item.label = "当前配置uid:\{" + Record.uid + "\}\n(仅仅显示配置uid无其他作用)"
+ })
+ let filterSettings = []
+ const filterUidSettings = uidSettings.filter(item => item?.name?.startsWith(levelName))
+ let tempFilterUidSettings = []
+
+ let last_parent_level = undefined
+ for (let i = 0; i < filterUidSettings.length; i++) {
+ let item = filterUidSettings[i]
+ // log.warn(`item:{0}`,item)
+ if (item?.name?.startsWith(levelName)) {
+ if (i === 0) {
+ tempFilterUidSettings.push({type: "separator"})
+ tempFilterUidSettings.push({type: "separator"})
+ }
+
+ tempFilterUidSettings.push({type: "separator"})
+
+ let parent_level = item?.name?.replace(levelName + "_", "")?.split("_")[0] || undefined
+ if (i !== 0 && parent_level && parent_level !== last_parent_level) {
+ tempFilterUidSettings.push({type: "separator"})
+ }
+ tempFilterUidSettings.push(item)
+ last_parent_level = parent_level
+ }
+ }
+ // log.debug("用户配置: {0}", tempFilterUidSettings)
+ filterSettings = tempFilterUidSettings
+ // filterSettings = filterUidSettings
+ // templateMatchSettings = Array.from(new Set(templateMatchSettings).difference(new Set(filterUidSettings)))
+ try {
+ loadingLevel = parseInt(settings.loading_level)
+ } catch (e) {
+ log.warn("配置 {0} 错误,将使用默认值{0}", "加载路径层级", loadingLevel)
+ } finally {
+ //
+ loadingLevel = loadingLevel < 1 ? 2 : loadingLevel
+ }
+ //todo: 高阶层级过滤
+ const highLevelFiltering = settings.high_level_filtering || undefined
+ if (highLevelFiltering && highLevelFiltering?.trim() !== "") {
+ /**
+ * 实例:pathing\地方特产\
+ * 地方特产
+ * 实例:pathing\地方特产\枫丹\
+ * 地方特产->枫丹
+ * 实例:pathing\地方特产\枫丹\幽光星星\
+ * 地方特产->枫丹->幽光星星
+ * 实例:pathing\地方特产\枫丹\幽光星星\幽光星星@jbcaaa\
+ * 地方特产->枫丹->幽光星星->幽光星星@jbcaaa
+ */
+ let keys = new Set([])
+
+ if (highLevelFiltering) {
+ const set = new Set(highLevelFiltering.split("->"));
+ keys = keys.union(set)
+ }
+
+ // function countMatchingElements(mainSet, subset) {
+ // const mainSetObj = new Set(mainSet);
+ // return subset.filter(item => mainSetObj.has(item)).length;
+ // }
+
+ // const key = keys[keys.size - 1]
+ // PATH_JSON_LIST.filter(item => item.level > 0)
+ // 预先建立 levelName 到路径信息的映射
+ const levelNameMap = new Map();
+ PATH_JSON_LIST.forEach(item => {
+ log.debug(`item:{0}`, JSON.stringify(item))
+ if (item.levelName) {
+ levelNameMap.set(item.levelName, item);
+ }
+ });
+ log.warn("levelNameMap:{0}", JSON.stringify([...levelNameMap]))
+ //中间一段路径名称
+ const dir_key = Array.from(keys).join("\\")
+ filterSettings = filterSettings.filter(item => {
+ if (!item?.name?.startsWith(levelName)) {
+ return true
+ }
+ // const settings_level = PATH_JSON_LIST.filter(list_item => list_item.levelName === item.name).find();
+ const settings_level = levelNameMap.get(item.name);
+ if (settings_level) {
+ //只加载指定目录
+ return (settings_level.path.includes(dir_key))
+ }
+ return false
+ })
+ }
+ const theLayer = settings.the_layer || false
+ const levelSettings = filterSettings.filter(item => {
+ if (!item?.name?.startsWith(levelName)) {
+ return true
+ }
+ const level_all = item.name.replaceAll(levelName + "_", "");
+ // 获取级别
+ const level = level_all.split("_").map(parseInt)[0]
+ if (theLayer) {
+ //只加载指定级别的设置
+ return (loadingLevel === level + 1)
+ }
+ // 检查级别是否小于等于加载层级
+ return (loadingLevel > level - 1)
+ })
+ templateMatchSettings = [...templateMatchSettings, ...levelSettings]
+ while (templateMatchSettings.length > 0 &&
+ templateMatchSettings[templateMatchSettings.length - 1]?.type === "separator") {
+ templateMatchSettings.pop();
+ }
+
+ /**
+ * 限制连续的分隔符数量不超过3个
+ * @param {Array} settings - 设置项数组
+ * @returns {Array} 处理后的设置项数组
+ */
+ function limitConsecutiveSeparators(settings) {
+ if (!Array.isArray(settings) || settings.length === 0) {
+ return settings;
+ }
+
+ const result = [];
+ let consecutiveSeparatorCount = 0;
+
+ for (const item of settings) {
+ if (item?.type === "separator") {
+ consecutiveSeparatorCount++;
+
+ // 只有当连续分隔符数量不超过3个时才添加
+ if (consecutiveSeparatorCount <= 3) {
+ result.push(item);
+ }
+ } else {
+ // 遇到非分隔符时重置计数
+ consecutiveSeparatorCount = 0;
+ result.push(item);
+ }
+ }
+
+ return result;
+ }
+
+ templateMatchSettings = limitConsecutiveSeparators(templateMatchSettings)
+ // uidSettings.push(levelSettings)
+ // 将更新后的设置写入配置文件
+ file.writeTextSync(manifest.settings_ui, JSON.stringify(templateMatchSettings))
+ } catch (e) {
+ // 记录错误日志
+ log.error("加载用户配置失败: {error}", e.message)
+ }
+ }
+ // 初始化配置设置
+ configSettings = await initSettings()
+}
+
+async function initRun(config_run) {
+ if (!loadPathJsonListByUid()) {
+ throw new Error(
+ "未找到 PATH_JSON_LIST,请先执行一次【刷新配置】"
+ );
+ }
+ log.info(`初始{0}配置`, config_run)
+ const cdPath = json_path_name.cdPath;
+ const timeJson = (!cd.open) ? new Set() : new Set(JSON.parse(file.readTextSync(cdPath)).sort(
+ (a, b) => b.level - a.level
+ ))
+
+ const multiCheckboxMap = await getMultiCheckboxMap();
+ if (dev.isDebug) {
+ await debugKey('[init-run]_log-multiCheckboxMap.json', JSON.stringify(Array.from(multiCheckboxMap)))
+ const keysList = Array.from(multiCheckboxMap.keys());
+ await debugKey('[init-run]_log-keysList.json', JSON.stringify(keysList))
+ }
+
+ settingsNameList = settingsNameList.concat(
+ Array.from(multiCheckboxMap.keys().filter(key =>
+ typeof key === "string" &&
+ key.startsWith(levelName) &&
+ multiCheckboxMap.get(key)?.options?.length > 0
+ ))
+ )
+
+
+ settingsNameList = settingsNameList
+ .filter(key => typeof key === "string" && key.trim() !== "")
+
+ log.debug(`settingsNameList:{0}`, JSON.stringify(settingsNameList))
+
+
+ // todo:补齐执行前配置
+ // ================= 执行前配置(补齐 needRunMap) =================
+ await debugKey(
+ '[init-run]_log-PATH_JSON_LIST.json',
+ JSON.stringify(PATH_JSON_LIST)
+ );
+
+ for (const settingsName of settingsNameList) {
+
+ // 1. 读取 multi-checkbox 的 JSON 描述
+ const multiJson = await getJsonByMultiCheckboxName(settingsName);
+ if (!multiJson || !multiJson.options || multiJson.options.length === 0) continue;
+
+ const labelParentName = getBracketContent(multiJson.label); // [xxx]
+ const selectedOptions = multiJson.options;
+
+ // 2. 从 PATH_JSON_LIST 中筛选命中的路径
+ const filter = PATH_JSON_LIST.filter(item => item.children.length === 0);
+ await debugKey(`[init-run]_log-filtermatchedPaths.json`, JSON.stringify(filter))
+ let matchedPaths = filter.filter(item => {
+ const hitParent = item.fullPathNames.includes(labelParentName) || labelParentName === `${pathingName}`;
+ const hitOption = selectedOptions.some(opt =>
+ item.fullPathNames.some(name => name.includes(opt))
+ );
+
+ return hitParent && hitOption;
+ }).map(item => {
+ const selected = selectedOptions.find(opt =>
+ item.fullPathNames.some(name => name.includes(opt))
+ );
+
+ return {
+ level: item.level,
+ name: item.name,
+ id: item.id,
+ parentId: item.parentId,
+ parentName: item.parentName,
+ rootName: item.rootName,
+ selected: selected,
+ path: item.path,
+ fullPathNames: item.fullPathNames
+ };
+ });
+ await debugKey(`[init-run]_log-matchedPaths.json`, JSON.stringify(matchedPaths))
+
+ function generatedKey(item, useParent = false) {
+ const separator = "->";
+
+ if (useParent) {
+ // 使用父级名称的逻辑
+ if (item?.parent_name && !item?.parentName) {
+ return `${item.parent_name}${separator}${item.name}`;
+ } else if (item?.parentName) {
+ return `${item.parentName}${separator}${item.name}`;
+ }
+ } else {
+ // 三层结构的逻辑
+ if (item?.rootName && item?.parentName && item?.rootName !== "" && item?.parentName !== item?.rootName) {
+ return `${item.rootName}${separator}${item.parentName}${separator}${item.name}`;
+ } else if (item?.root_name && item?.parent_name && item?.root_name !== "" && item?.parent_name !== item?.root_name) {
+ return `${item.root_name}${separator}${item.parent_name}${separator}${item.name}`;
+ }
+ // 二层结构的逻辑
+ if (item?.parent_name && !item?.parentName) {
+ return `${item.parent_name}${separator}${item.name}`;
+ } else if (item?.parentName) {
+ return `${item.parentName}${separator}${item.name}`;
+ }
+ }
+
+ // 默认返回名称
+ return item.name;
+ }
+
+ // 3. CD 过滤(可选)
+ if (cd.open && matchedPaths.length > 0) {
+ let recordPaths = [];
+ try {
+ recordPaths = Array.from(RecordPath.paths);
+ } catch (e) {
+ log.error("读取记录路径失败: {error}", e.message)
+ }
+ recordPaths.sort((a, b) => b.timestamp - a.timestamp)
+
+ const timeConfigs = Array.from(timeJson);
+ await debugKey(`[init-run]_log-timeConfigs.json`, JSON.stringify(timeConfigs))
+ await debugKey(`[init-run]_log-recordPaths.json`, JSON.stringify(recordPaths))
+ let bodyList = []
+ const now = Date.now();
+ //首次过滤
+ let cdFilterMatchedPaths = matchedPaths.filter(item => {
+ const timeConfig = timeConfigs.find(cfg =>
+ item.fullPathNames.includes(cfg.name)
+ );
+ if (!timeConfig) return false;
+
+ const record = recordPaths.find(r =>
+ r.path.includes(item.path)
+ );
+ if (!record || !record.timestamp) return false;
+ switch (timeType.fromValue(timeConfig.type)) {
+ case timeType.cron:
+ // timeConfig.name
+ // const key = generatedKey(item);
+ const item_key = bodyList.find(cfg => cfg.key === item.path)
+ if (!item_key) {
+ bodyList.push({
+ key: item.path,
+ cronExpression: timeConfig.value,
+ startTimestamp: record.timestamp,
+ endTimestamp: now
+ })
+ } else if (item_key.startTimestamp < record.timestamp) {
+ item_key.startTimestamp = record.timestamp
+ item_key.cronExpression = timeConfig.value
+ }
+
+ return true;
+ default:
+ return true;
+ }
+ return true
+ })
+ await debugKey(`[init-run]_log-cdFilterMatchedPaths.json`, JSON.stringify(cdFilterMatchedPaths))
+ //多次请求改一次请求
+ const nextMap = bodyList.length <= 0 ? new Map() : await cronUtil.getNextCronTimestampAll(bodyList, cd.http_api) ?? new Map();
+ await debugKey(``, JSON.stringify({nextMap: [...nextMap]}), true)
+ //还在cd中的path
+ const in_cd_paths = cdFilterMatchedPaths.filter(async item => {
+ const timeConfig = timeConfigs.find(cfg =>
+ item.fullPathNames.includes(cfg.name)
+ );
+
+ switch (timeType.fromValue(timeConfig.type)) {
+ case timeType.hours: {
+ const diff = getTimeDifference(record.timestamp, now);
+ return (diff.total.hours < timeConfig.value);
+ }
+ case timeType.cron: {
+ // const next = await cronUtil.getNextCronTimestamp(
+ // `${timeConfig.value}`,
+ // record.timestamp,
+ // now,
+ // cd.http_api
+ // );
+ // return (next && now >= next);
+ // const key = generatedKey(item);
+ const cron_ok = nextMap.get(item.path)
+ return !(cron_ok?.ok); // 不应该在CD中时返回true
+ }
+ default:
+ return false;
+ }
+ });
+ await debugKey(`[init-run]_log-in_cd_paths.json`, JSON.stringify(in_cd_paths))
+ // 移除 CD 未到的路径
+ if (in_cd_paths.length > 0) {
+ const cdPathSet = new Set(in_cd_paths.map(item => item.path));
+ matchedPaths = matchedPaths.filter(item => !cdPathSet.has(item.path));
+ }
+
+ }
+
+ // 4. 写入 needRunMap
+ if (matchedPaths.length > 0) {
+ //锄地队对应
+ try {
+ // {
+ // uid:"",
+ // parent_name:"",
+ // root_name: "",
+ // name:"",
+ // team_name:""
+ // } json支持
+ let teamHoeGroundList = JSON.parse(file.readTextSync(json_path_name.HoeGround)) ?? [{
+ uid: "",
+ is_common: false,
+ parent_name: undefined,
+ root_name: undefined,
+ name: undefined,
+ team_name: ""
+ }]
+ teamHoeGroundList = teamHoeGroundList.filter(item => item?.uid === Record.uid || item?.is_common)
+ teamHoeGroundList.sort((a, b) => {
+ const orderA = a?.is_common ? 1 : 0;
+ const orderB = b?.is_common ? 1 : 0;
+ return orderB - orderA; // 这样 is_common 为 true 的会排在前面
+ });
+ // 自定义锄地队对应可覆盖公共锄地队对应
+ teamHoeGroundList.forEach(item => {
+ if (item?.root_name?.trim() !== "") {
+ const key = generatedKey(item);
+ team.HoeGroundMap.set(key, item.team_name);
+
+ } else {
+ const key_parent = generatedKey(item, true);
+ team.HoeGroundMap.set(key_parent, item.team_name);
+ }
+
+ })
+ log.info(`{0}加载完成`, json_path_name.HoeGround)
+ } catch (e) {
+ log.error(`加载失败:{0}`, e.message)
+ }
+ //输入值优先覆盖
+ try {
+ const teamHoeGroundStr = settings.team_hoe_ground || "parentName->name=key"
+ teamHoeGroundStr.split(",").forEach(item => {
+ const [key, team_name] = item.split("=");
+ team.HoeGroundMap.set(key, team_name)
+ })
+ } catch (e) {
+ log.error(`加载失败:{0}`, e.message)
+ }
+
+ // 排序
+ const orderMap = new Map()
+ try {
+ // {
+ // uid:"",
+ // parent_name:"",
+ // root_name: "",
+ // name:"",
+ // order:0
+ // } json支持
+ let orderList = JSON.parse(file.readTextSync(json_path_name.PathOrder)) ?? [{
+ uid: "",
+ is_common: false,
+ parent_name: undefined,
+ root_name: undefined,
+ name: undefined,
+ order: 0
+ }]
+ orderList = orderList.filter(item => item?.uid === Record.uid || item?.is_common)
+ orderList.sort((a, b) => {
+ const orderA = a?.is_common ? 1 : 0;
+ const orderB = b?.is_common ? 1 : 0;
+ return orderB - orderA; // 这样 is_common 为 true 的会排在前面
+ });
+ // 自定义排序可覆盖公共排序
+ orderList.forEach(item => {
+ if (item.root_name) {
+ const key = generatedKey(item);
+ orderMap.set(key, item.order)
+ } else {
+ const key_parent = generatedKey(item, true);
+ orderMap.set(key_parent, item.order)
+ }
+ })
+ log.info(`{0}加载完成`, json_path_name.PathOrder)
+ } catch (e) {
+ log.error(`加载失败:{0}`, e.message)
+ }
+ await debugKey("[init-run]_log-orderMap-By-json.json", JSON.stringify([...orderMap]))
+ //输入值优先覆盖
+ try {
+ // 支持多条规则,例如: "rootName->parentName->name1=1,rootName->parentName->name2=2"
+ const orderStr = settings.order_rules || "rootName->parentName->name=1"
+ orderStr.split(",").forEach(item => {
+ const [key, order] = item.split("=");
+ orderMap.set(key, parseInt(order))
+ })
+ } catch (e) {
+ log.error(`加载失败:{0}`, e.message)
+ }
+ await debugKey("[init-run]_log-orderMap-All.json", JSON.stringify([...orderMap]))
+ //限制组最大执行数
+ const openLimitMax = settings.open_limit_max
+ let limitMaxByGroup = new Map()
+ if (openLimitMax) {
+ log.info(`{0}`, '已开启限制组最大执行数')
+ try {
+ let limitMaxList = JSON.parse(file.readTextSync(json_path_name.LimitMax)) ?? [{
+ uid: "",
+ is_common: false,
+ parent_name: undefined,
+ root_name: undefined,
+ name: undefined,
+ max: 0
+ }]
+ limitMaxList = limitMaxList.filter(item => item?.uid === Record.uid || item?.is_common)
+ limitMaxList.sort((a, b) => {
+ const orderA = a?.is_common ? 1 : 0;
+ const orderB = b?.is_common ? 1 : 0;
+ return orderB - orderA; // 这样 is_common 为 true 的会排在前面
+ });
+ // 自定义排序可覆盖公共排序
+ limitMaxList.forEach(item => {
+ if (item?.root_name?.trim() !== "") {
+ const key = generatedKey(item);
+ limitMaxByGroup.set(key, parseInt(item.max))
+ log.debug(`limitMaxList=>{0}->{1}`, key, item.max)
+ } else {
+ const key_parent = generatedKey(item, true);
+ limitMaxByGroup.set(key_parent, parseInt(item.max))
+ log.debug(`limitMaxList=>{0}->{1}`, key_parent, item.max)
+ }
+ })
+
+ //输入值优先覆盖
+ try {
+ // 支持多条规则,例如: "rootName->parentName->name1=1,rootName->parentName->name2=2"
+ const limitMaxStr = settings.limit_max_group || "rootName->parentName->name=1"
+ limitMaxStr.split(",").forEach(item => {
+ const [key, max] = item.split("=");
+ limitMaxByGroup.set(key, parseInt(max))
+ })
+ } catch (e) {
+ log.error(`加载失败:{0}`, e.message)
+ }
+ } catch (e) {
+ log.error(`加载失败:{0}`, e.message)
+
+ }
+ }
+
+
+ function groupByParentAndName(list) {
+ const map = new Map();
+
+ list.forEach(item => {
+ // const key = `${item.parentName}->${item.name}`;
+ const key = generatedKey(item);
+ // const key_parent = generatedKey(item, true);
+ if (!map.has(key)) map.set(key, []);
+ map.get(key).push(item);
+ // if (!map.has(key_parent)) map.set(key_parent, []);
+ // map.get(key_parent).push(item);
+ });
+
+ return Array.from(map.values()); // 转成二维数组 [[], []]
+ }
+
+ let groups = groupByParentAndName(matchedPaths);
+ groups.sort((a, b) => {
+ const a_key = generatedKey(a)
+ const b_key = generatedKey(b)
+ const orderA = orderMap.get(a_key) ?? 0; // 没在 JSON 中的排到最后
+ const orderB = orderMap.get(b_key) ?? 0;
+ if (orderA === orderB) {
+ return a_key?.localeCompare(b_key);
+ }
+ return orderB - orderA; // 修改为倒序数字比较
+ })
+ const asMap = new Map()
+ groups.forEach(group => {
+ const groupOne = group[0]
+ let groupKey_parent = generatedKey(groupOne, true);
+ if (orderMap.has(groupKey_parent) || team.HoeGroundMap.has(groupKey_parent)) {
+ let groupKey = generatedKey(groupOne);
+ asMap.set(groupKey, groupKey_parent)
+ }
+ })
+ groups.forEach(group => {
+ const groupOne = group[0]
+ let groupKey = generatedKey(groupOne);
+ let key = groupKey
+ if (asMap.has(key)) {
+ key = asMap.get(key)
+ }
+ let runGroup = group
+ //限制组最大执行数
+ if (openLimitMax && (limitMaxByGroup.has(key) || limitMaxByGroup.has(groupKey))) {
+ const limitMax = (limitMaxByGroup.get(key) || limitMaxByGroup.get(groupKey)) ?? 99999
+ const max = Math.min(group.length, limitMax)
+ runGroup = group.slice(0, max)
+ log.debug("[限制组最大执行数] groupKey={0},max={1},limitMax={2},group.length={3}", key, max, limitMax, group.length)
+ }
+ needRunMap.set(key, {
+ order: (orderMap.get(key) || orderMap.get(groupKey)) ?? 0,
+ paths: runGroup,
+ parent_name: groupOne.parentName,
+ key: key,
+ current_name: groupOne.name,
+ name: settingsName //多选项 名称 如 treeLevel_0_0
+ });
+ })
+ // todo 对 needRunMap 进行排序
+ // 对 needRunMap 进行整体排序
+ const sortedNeedRunMap = new Map(
+ [...needRunMap.entries()].sort((a, b) => {
+ // 使用值中的 order 字段进行排序
+ const orderA = a[1].order ?? 0;
+ const orderB = b[1].order ?? 0;
+
+ // 按降序排序(数值大的优先)
+ return orderB - orderA;
+ })
+ );
+
+ // 替换原来的 needRunMap
+ needRunMap.clear();
+ for (const [key, value] of sortedNeedRunMap) {
+ needRunMap.set(key, value);
+ }
+
+
+ await debugKey(
+ '[init-run]_log-needRunMap.json',
+ JSON.stringify([...needRunMap])
+ );
+ }
+ log.info("[执行前配置完成] needRunMap.size={0}", needRunMap.size);
+ }
+}
+
+async function init() {
+ let settingsConfig = await initSettings(`${config_root}\\`);
+ let utils = [
+ "cron",
+ "SwitchTeam",
+ "uid",
+ ]
+ for (let util of utils) {
+ eval(file.readTextSync(`utils/${util}.js`));
+ }
+ if (manifest.key !== settings.key) {
+ let message = "密钥不匹配";
+ if (settings.key) {
+ message += ",脚本可能存在升级 密钥已经改变 请查看文档功能后重新获取密钥"
+ }
+ throw new Error(message)
+ }
+ auto.semi = settings.mode === "半自动"
+ if (auto.semi) {
+ auto.run = settings.auto_semi_key_mode === "继续运行"
+ auto.skip = settings.auto_semi_key_mode === "跳过"
+ auto.key = settings.auto_key
+ if (!auto.key) {
+ throw new Error(settings.mode + "模式下必须开启快捷键设置")
+ }
+ }
+
+ dev.debug = (dev.debug) ? dev.debug : settings.debug
+ dev.isDebug = settings.is_debug
+
+ cd.open = (cd.open) ? cd.open : settings.cd_open
+ cd.http_api = (cd.http_api) ? cd.http_api : settings.http_api
+ await debugKey("[init]_log-cd-settings.json", JSON.stringify(cd))
+ config_list.black = settings.config_black_list ? settings.config_black_list.split(",") : []
+ config_list.white = settings.config_white_list ? settings.config_white_list.split(",") : []
+
+
+ if (!file.IsFolder(`${pathingName}`)) {
+ let batFile = "SymLink.bat";
+ log.error("{0}文件夹不存在,请在BetterGI中右键点击本脚本,选择{1}。然后双击脚本目录下的{2}文件以创建文件夹链接", `${pathingName}`, "打开所在目录", batFile);
+ return false;
+ }
+ try {
+ let parse = JSON.parse(file.readTextSync(json_path_name.SevenElement));
+ if (parse) {
+ parse?.sort((a, b) => a.level - b.level)
+ SevenElement.SevenElements = Array.from(new Set(SevenElement.SevenElements.concat(parse.map(item => item.name))))
+ parse.forEach(item => {
+ const name = item.name
+ let value = item.value
+ if (SevenElement.SevenElementsMap.has(name)) {
+ value = Array.from(new Set(SevenElement.SevenElementsMap.get(name).concat(value)))
+ }
+ SevenElement.SevenElementsMap.set(name, value)
+ })
+ }
+ } catch (e) {
+ log.warn("[SevenElement]初始化失败error:{0}", e.message)
+
+ }
+ //记录初始化
+ await initRecord();
+
+ // 读取现有配置并合并
+ let uidSettingsMap = new Map()
+ uidSettingsMap = await initUidSettingsMap(uidSettingsMap);
+
+//总控
+//刷新settings
+ const config_run = settings.config_run;
+ log.info("开始执行配置: {config_run}", config_run)
+ if (config_run === "刷新") {
+ await initRefresh(settingsConfig);
+ log.info("配置{0}完成", config_run)
+ } else if (config_run === "加载") {
+ //直接从配置文件中加载对应账号的配置
+ await loadUidSettingsMap(uidSettingsMap);
+ log.info("配置{0}完成", config_run)
+ } else
+ // 初始化needRunMap
+ if (config_run === "执行") {
+ await initRun(config_run);
+ await realTimeMissions()
+ }
+ return true
+}
+
+(async function () {
+ try {
+ if (await init()) {
+ await main()
+ }
+ } finally {
+ await saveRecordPaths();
+ await saveRecord();
+ }
+
+})()
+
+async function main() {
+ let lastRunMap = new Map()
+
+ function chooseBestRun() {
+ if (settings.choose_best && RecordLast.paths.size > 0) {
+ // 由于在迭代过程中删除元素会影响迭代,先收集要删除的键
+ const keysToDelete = [];
+ // 优先跑上次没跑过的路径
+ // 使用 Set 提高性能
+ const lastListSet = new Set([...RecordLast.paths]);
+
+ for (const [key, one] of needRunMap.entries()) {
+ // 检查当前任务的路径是否都不在上次执行的路径中
+ const allPathsInLast = one.paths.every(pathObj => lastListSet.has(pathObj.path));
+
+ if (!allPathsInLast) {
+ lastRunMap.set(key, one);
+ keysToDelete.push(key);
+ }
+ }
+
+
+ // 然后批量删除
+ for (const key of keysToDelete) {
+ needRunMap.delete(key);
+ }
+ //保持排序
+ // 对 lastRunMap 进行整体排序
+ const sortedNeedRunMap = new Map(
+ [...lastRunMap.entries()].sort((a, b) => {
+ // 使用值中的 order 字段进行排序
+ const orderA = a[1].order ?? 0;
+ const orderB = b[1].order ?? 0;
+
+ // 按降序排序(数值大的优先)
+ return orderB - orderA;
+ })
+ );
+
+ // 替换原来的 lastRunMap
+ lastRunMap.clear();
+ for (const [key, value] of sortedNeedRunMap) {
+ lastRunMap.set(key, value);
+ }
+ }
+ }
+
+ chooseBestRun();
+ await debugKey("[run]_log-lastRunMap.json", JSON.stringify([...lastRunMap]))
+ await debugKey("[run]_log-needRunMap.json", JSON.stringify([...needRunMap]))
+
+ if (needRunMap.size > 0) {
+ if (settings.choose_best) {
+ log.info(`[{0}] [{1}]`, settings.mode, "择优模式-启动")
+ }
+ await runMap(needRunMap)
+ }
+ if (lastRunMap.size > 0) {
+ log.info(`[{0}] [{1}]`, settings.mode, "择优模式-收尾")
+ await runMap(lastRunMap)
+ }
+
+ // if (needRunMap.size <= 0 && lastRunMap.size <= 0) {
+ // log.info(`设置目录{0}完成`, "刷新")
+ // }
+ // log.info(`[{mode}] path==>{path},请按下{key}以继续执行[${manifest.name} JS]`, settings.mode, "path", AUTO_STOP)
+ // await keyMousePressStart(AUTO_STOP);
+ // log.info(`[{mode}] path==>{path},请按下{key}以继续执行[${manifest.name} JS]`, settings.mode, "path", AUTO_STOP)
+}
+
+/**
+ * 保存记录路径的函数
+ * 该函数将RecordPath对象中的Set类型数据转换为数组后保存到文件
+ */
+async function saveRecordPaths() {
+ // 保存前将 Set 转换为数组,因为JSON不支持Set类型
+ // 创建一个新的记录对象,包含原始记录的所有属性
+ const recordToSave = {
+ // 使用展开运算符复制Record对象的所有属性,保持其他数据不变
+ ...RecordPath,
+ // 处理 paths 数组
+ paths: (() => {
+ // 1. 使用 Map 来辅助去重,Map 的 key 是 path,value 是完整的 item 对象
+ const pathMap = new Map();
+
+ // 假设 RecordPath.paths 是一个 Set,先转为数组进行遍历
+ [...RecordPath.paths].forEach(item => {
+ // 获取当前项的路径字符串
+ const currentPath = item.path;
+
+ // 检查 Map 中是否已经存在该路径
+ if (pathMap.has(currentPath)) {
+ // 如果存在,比较时间戳
+ const existingItem = pathMap.get(currentPath);
+ // 如果当前项的时间戳比已存在的大,则更新 Map 中的值
+ if (item.timestamp > existingItem.timestamp) {
+ pathMap.set(currentPath, item);
+ }
+ } else {
+ // 如果不存在,直接存入 Map
+ pathMap.set(currentPath, item);
+ }
+ });
+
+ // 2. 将 Map 中的值(去重后的对象数组)转换回我们需要的格式
+ return Array.from(pathMap.values()).map(item => ({
+ timestamp: item.timestamp,
+ path: item.path
+ }));
+ })()
+ };
+
+ // 确保 RecordPathList 是数组
+ if (!Array.isArray(RecordPathList)) {
+ RecordPathList = Array.from(RecordPathList);
+ }
+ let temp = RecordPathList.find(item => item.uid === Record.uid)
+ if (temp) {
+ // RecordList.splice(RecordList.indexOf(temp),1)
+ temp.paths = [...recordToSave.paths, ...temp.paths]
+ // temp.errorPaths = [...recordToSave.errorPaths, ...temp.errorPaths]
+ // temp.groupPaths = [...recordToSave.groupPaths, ...temp.groupPaths]
+ } else {
+ // 将记录对象添加到记录列表中
+ RecordPathList.push(recordToSave)
+ }
+ // 将记录列表转换为JSON字符串并同步写入文件
+ file.writeTextSync(json_path_name.RecordPathText, JSON.stringify(RecordPathList))
+ log.info("saveRecordPath保存记录文件成功")
+}
+
+/**
+ * 保存当前记录到记录列表并同步到文件
+ * 该函数在保存前会将Set类型的数据转换为数组格式,确保JSON序列化正常进行
+ */
+async function saveRecord() {
+ // 保存前将 Set 转换为数组
+ // 创建一个新的记录对象,包含原始记录的所有属性
+ const recordToSave = {
+ // 使用展开运算符复制Record对象的所有属性
+ ...Record,
+ // 将paths Set转换为数组
+ paths: [...Record.paths],
+ // 将errorPaths Set转换为数组
+ errorPaths: [...Record.errorPaths],
+ // 将groupPaths Set转换为数组,并对每个元素进行特殊处理
+ groupPaths: [...Record.groupPaths].map(item => ({
+ // 保留name属性
+ name: item.name,
+ // 将item中的paths Set转换为数组
+ paths: [...item.paths]
+ }))
+ };
+
+ // 将记录对象添加到记录列表中
+ RecordList.push(recordToSave)
+ // 将记录列表转换为JSON字符串并同步写入文件
+ file.writeTextSync(json_path_name.RecordText, JSON.stringify(RecordList))
+ log.info("saveRecord保存记录文件成功")
+}
+
+/**
+ * 计算两个时间之间的差值,并返回指定格式的JSON
+ * @param {number|Date} startTime - 开始时间(时间戳或Date对象)
+ * @param {number|Date} endTime - 结束时间(时间戳或Date对象)
+ * @returns {Object} diff_json - 包含info和total的对象
+ */
+function getTimeDifference(startTime, endTime) {
+ // 确保输入是时间戳
+ const start = typeof startTime === 'object' ? startTime.getTime() : startTime;
+ const end = typeof endTime === 'object' ? endTime.getTime() : endTime;
+
+ // 计算总差值(毫秒)
+ const diffMs = Math.abs(end - start);
+
+ // 计算总时间(小数)
+ const totalSeconds = diffMs / 1000;
+ const totalMinutes = totalSeconds / 60;
+ const totalHours = totalSeconds / 3600;
+
+ // 计算info部分(整数)
+ const infoHours = Math.floor(totalHours % 24);
+ const remainingAfterHours = (totalHours % 24) - infoHours;
+ const infoMinutes = Math.floor(remainingAfterHours * 60);
+ const remainingAfterMinutes = (remainingAfterHours * 60) - infoMinutes;
+ const infoSeconds = Math.floor(remainingAfterMinutes * 60);
+// 输出类似:
+// {
+// info: { hours: 1, minutes: 0, seconds: 0 },
+// total: { hours: 1, minutes: 60, seconds: 3600 }
+// }
+ const diff_json = {
+ info: {
+ hours: infoHours,
+ minutes: infoMinutes,
+ seconds: infoSeconds
+ },
+ total: {
+ hours: parseFloat(totalHours.toFixed(6)),
+ minutes: parseFloat(totalMinutes.toFixed(6)),
+ seconds: parseFloat(totalSeconds.toFixed(6))
+ }
+ };
+
+ return diff_json;
+}
+
+
+/**
+ * 初始化记录函数
+ * 该函数用于初始化一条新的记录,包括设置UID、时间戳和调整后的日期数据
+ * 同时会检查记录列表中是否存在相同UID的最新记录,并进行更新
+ */
+async function initRecord() {
+ // 设置记录的唯一标识符,通过OCR技术获取
+ Record.uid = await uidUtil.ocrUID()
+ // 设置记录的时间戳为当前时间
+ Record.timestamp = Date.now()
+ // 获取并设置调整后的日期数据
+ Record.data = getAdjustedDate()
+
+ try {
+ // 尝试读取记录文件
+ // 读取后将数组转换回 Set,处理特殊的数据结构
+ RecordList = JSON.parse(file.readTextSync(json_path_name.RecordText), (key, value) => {
+ // 处理普通路径集合
+ if (key === 'paths' || key === 'errorPaths') {
+ return new Set(value);
+ }
+ // 处理分组路径集合,保持嵌套的Set结构
+ if (key === 'groupPaths') {
+ return new Set(value.map(item => ({
+ name: item.name,
+ paths: new Set(item.paths)
+ })));
+ }
+ return value;
+ }) ?? RecordList;
+ } catch (e) {
+ // 如果读取文件出错,则忽略错误(可能是文件不存在或格式错误)
+ }
+ try {
+ // 尝试读取记录文件
+ // 读取后将数组转换回 Set,处理特殊的数据结构
+ RecordPathList = JSON.parse(file.readTextSync(json_path_name.RecordPathText), (key, value) => {
+ // 处理分组路径集合,保持嵌套的Set结构
+ if (key === 'paths') {
+ return new Set(value.map(item => ({
+ timestamp: item.timestamp,
+ path: item.path
+ })));
+ }
+ return value;
+ }) ?? RecordPathList
+ RecordPath = RecordPathList.find(item => item.uid === Record.uid) ?? RecordPath
+ } catch (e) {
+ // 如果读取文件出错,则忽略错误(可能是文件不存在或格式错误)
+ }
+ RecordPath.uid ||= Record.uid
+ RecordPath.paths ||= new Set()
+ // 如果记录列表不为空,则查找最新记录
+ if (RecordList.length > 0) {
+ // 最优解:一次遍历找到最新的记录
+ let latestRecord = undefined;
+ // 遍历记录列表,查找相同UID且时间戳最大的记录
+ for (const item of RecordList) {
+ // 检查当前记录项的UID是否匹配,并且是最新记录
+ if (item.uid === Record.uid &&
+ // 如果还没有找到记录,或者当前记录的时间戳比已找到的记录更新
+ (!latestRecord || item.timestamp > latestRecord.timestamp)) {
+ // 更新最新记录为当前项
+ latestRecord = item;
+ }
+ }
+ // 如果找到最新记录,则更新RecordLast;否则保持原有RecordLast的值
+ RecordLast = latestRecord ? latestRecord : RecordLast;
+ }
+ if (RecordLast.uid === Record.uid && Record.data === RecordLast.data) {
+ // 判断是否为同一天 合并跑过的数据
+ // 确保 RecordLast 的 Set 属性存在
+ if (!RecordLast.paths || !(RecordLast.paths instanceof Set)) {
+ RecordLast.paths = new Set();
+ }
+ if (!RecordLast.groupPaths || !(RecordLast.groupPaths instanceof Set)) {
+ RecordLast.groupPaths = new Set();
+ }
+
+ // 判断是否为同一天 合并跑过的数据
+ Record.paths = new Set([...Record.paths, ...RecordLast.paths])
+ Record.groupPaths = new Set([...Record.groupPaths, ...RecordLast.groupPaths])
+ // 删除RecordLast
+ const index = RecordList.indexOf(RecordLast);
+ if (index > -1) {
+ RecordList.splice(index, 1);
+ }
+ }
+}
+
+function getAdjustedDate() {
+ const now = new Date();
+ // 减去4小时(4 * 60 * 60 * 1000 毫秒)
+ now.setHours(now.getHours() - 4);
+
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, '0');
+ const day = String(now.getDate()).padStart(2, '0');
+
+ return `${year}-${month}-${day}`;
+}
+
+// 解析日期字符串
+function parseDate(dateString) {
+ const [year, month, day] = dateString.split('-').map(Number);
+ return new Date(year, month - 1, day); // month - 1 因为月份从0开始
+}
+
+// Record.groupPaths.add({
+// name: "",
+// paths:new Set()
+// })
+// const pathingALLSize = []
+// 使用函数来添加唯一元素
+// 优化后的函数
+function addUniquePath(obj, list = PATHING_ALL) {
+ const existingIndex = list.findIndex(item =>
+ item.level === obj.level && item.name === obj.name && item.parentName === obj.parentName && item.rootName === obj.rootName
+ );
+
+ if (existingIndex === -1) {
+ list.push(obj);
+ } else {
+ // 合并 child_names 数组,避免重复元素
+ const existingItem = list[existingIndex];
+ if (obj.child_names) {
+ const newChildren = obj.child_names || [];
+
+ // 使用 Set 去重并合并数组
+ const combinedChildren = [...new Set([
+ ...(existingItem.child_names || []),
+ ...newChildren
+ ])];
+
+ existingItem.child_names = combinedChildren;
+ } else {
+ const newChildren = obj.children || [];
+
+ // 使用 Set 去重并合并数组
+ const combinedChildren = [...new Set([
+ ...(existingItem.children || []),
+ ...newChildren
+ ])];
+
+ existingItem.children = combinedChildren;
+ }
+
+ }
+}
+
+/**
+ * 初始化设置函数
+ * 从配置文件中读取设置信息并返回
+ * @returns {Object} 返回解析后的JSON设置对象
+ */
+async function initSettings(prefix = undefined) {
+ // 默认设置文件路径
+ let settings_ui = "settings.json";
+ if (!configSettings) {
+ try {
+ // 读取并解析manifest.json文件
+ manifest = manifest ? manifest : JSON.parse(file.readTextSync(manifest_json));
+ // 调试日志:输出manifest内容
+ // log.debug("manifest={key}", manifest);
+ // 调试日志:输出manifest中的settings_ui配置
+ log.debug("settings_ui={key}", manifest.settings_ui);
+ log.info(`|脚本名称:{name},版本:{version}`, manifest.name, manifest.version);
+ if (manifest.bgi_version) {
+ log.info(`|最小可执行BGI版本:{bgi_version}`, manifest.bgi_version);
+ }
+ log.info(`|脚本作者:{authors}\n`, manifest.authors.map(a => a.name).join(","));
+ // 更新settings_ui变量为manifest中指定的路径
+ settings_ui = manifest.settings_ui
+ settings_ui = prefix ? prefix + settings_ui : settings_ui
+ } catch (error) {
+ // 捕获并记录可能的错误
+ log.warn("{error}", error.message);
+ }
+ }
+ // 读取并解析设置文件
+ const settingsJson = JSON.parse(file.readTextSync(settings_ui));
+ // 如果configSettings未定义,则将其设置为解析后的设置对象
+ if (!configSettings) {
+ configSettings = settingsJson
+ }
+ // 调试日志:输出最终解析的设置对象
+ log.debug("settingsJson={key}", settingsJson);
+ // 返回设置对象
+ return settingsJson
+}
+
+/**
+ * 获取多复选框的映射表
+ * 该函数会从初始化的设置中提取所有类型为"multi-checkbox"的条目,
+ * 并将这些条目的名称和对应的选项值存储在一个Map对象中返回
+ * @returns {Promise