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存储) 先获取版本密钥 +
+ ![init-re](md/init-re.jpg)
+- 2.执行 + **选择 全自动**
+ ![run-init-01](md/run-init-01.jpg)
+ **CD算法需要部署 [bettergi-scripts-tools](https://github.com/Kirito520Asuna/bettergi-scripts-tools/releases) 并启用http
+ 也可以开启择优模式(择优模式不依赖bettergi-scripts-tools)**
+ ![run-init-01](md/run-init-cd-01.jpg)
+ ![init-http](md/init-http.jpg)
+ **日常选择 全自动**
+ ![init-auto](md/init-auto.jpg)
+ **配置队伍**
+ 配置默认 行走队
+ 队伍配置 实例语法对应映射: 矿物,火,水,风,雷,草,冰,岩
+ 队伍配置 实例语法: 矿采集,水火风雷,水火风雷,水火风雷,元素,矿采集
+ 没有可留空 如 矿采集,,,,,矿采集
+ **锄地队伍配置**
+ 实例语法: 敌人与魔物->发条机关=速通,..... (还不清楚的看`锄地队伍配置图映射`)
+ ![init-team](md/init-team.jpg)
+ **`锄地队伍配置图映射`**
+ ![init-team-map](md/init-team-map.jpg)
+ **`选中执行组`**
+ 多选项 为或关系
+ 如选中 地方特产的璃月 和 璃月的石珀 会执行 地方特产的璃月下所有的路径
+ 如需要指定 请勿选中上级目录 璃月的石珀(3级路径)->地方特产的璃月(2级路径)
+ 如图选中下3级路径时不要选中对应的2级路径
+ ![init-run-pathing](md/init-run-pathing.jpg)
+ ![init-run-pathing-01](md/init-run-pathing-01.jpg)
+- 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 * ?" + } +] +``` +## 运行实例部分日志 +实例==>运行矿物 大类 虹滴晶 运行完成 再次运行 日志截图展示: +![bettergi-scripts-tools](md/F-md.jpg) +## 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} 返回一个Promise对象,解析为包含多复选框配置的Map + */ +async function getMultiCheckboxMap() { + // 如果configSettings存在则使用它,否则调用initSettings()函数获取 + const settingsJson = await initSettings(); + // 创建一个新的Map对象用于存储多复选框的配置 + // Map结构为: {名称: 选项数组} + let multiCheckboxMap = new Map([]); + // await debugKey("log-MultiCheckbox-settings.json",JSON.stringify(settingsJson)) + // 遍历设置JSON中的每个条目 + settingsJson.forEach((entry) => { + // 如果条目没有name属性或者类型不是"multi-checkbox",则跳过该条目 + if (!entry.name || entry.type !== "multi-checkbox") return; + // 解构条目中的name和label属性,便于后续使用 + const {name, label} = entry; + // 获取当前name对应的设置值,如果存在则转换为数组,否则使用空数组 + const options = settings[name] ? Array.from(settings[name]) : []; + if (options.length > 0) { + // 记录调试信息,包含名称、标签、选项和选项数量 + log.debug("name={key1},label={key2},options={key3},length={key4}", name, label, JSON.stringify(options), options.length); + // 将名称和对应的选项数组存入Map + // multiCheckboxMap.set(name, options); + multiCheckboxMap.set(name, {label: `${label}`, options: options}); + } + }) + log.debug("multiCheckboxMap={key}", JSON.stringify(Array.from(multiCheckboxMap))) + // 返回包含多复选框配置的Map + return multiCheckboxMap +} + +/** + * 根据多选框名称获取对应的JSON数据 + * 该函数是一个异步函数,用于从复选框映射表中获取指定名称的值 + * @param {string} name - 多选框的名称,用于在映射表中查找对应的值 + * @returns {Promise} 返回一个Promise,解析后为找到的值,如果未找到则返回undefined + */ +async function getJsonByMultiCheckboxName(name) { + // 获取复选框映射表,这是一个异步操作 + let multiCheckboxMap = await getMultiCheckboxMap() + // 从映射表中获取并返回指定名称对应的值 + return multiCheckboxMap.get(name) +} + +/** + * 根据复选框组名称获取对应的值 + * 这是一个异步函数,用于从复选框映射中获取指定名称的值 + * @param {string} name - 复选框组的名称 + * @returns {Promise} 返回一个Promise,解析为复选框组对应的值 + */ +async function getValueByMultiCheckboxName(name) { + // 获取复选框映射表,这是一个异步操作 + let multiCheckboxMap = await getMultiCheckboxMap() + // 从映射表中获取并返回指定名称对应的值 + return multiCheckboxMap.get(name).options +} + + +/** + * 获取字符串中第一个方括号对之间的内容 + * @param {string} str - 输入的字符串 + * @returns {string} 返回第一个方括号对之间的内容,如果没有找到则返回空字符串 + */ +function getBracketContent(str) { + // 查找第一个 [ 和最后一个 ] 的位置 + const firstBracketIndex = str.indexOf('['); + const lastBracketIndex = str.lastIndexOf(']'); + + // 检查是否找到了有效的括号对 + if (firstBracketIndex !== -1 && lastBracketIndex !== -1 && firstBracketIndex < lastBracketIndex) { + // 提取第一个 [ 和最后一个 ] 之间的内容 + return str.substring(firstBracketIndex + 1, lastBracketIndex); + } + + // 如果没有找到有效的括号对,返回空字符串 + return ''; +} + + +/** + * 调试按键函数,用于在开发者模式下暂停程序执行并等待特定按键 + * @param {string} key - 需要按下的键 + * @param {string} path - 调试信息保存的文件路径,默认为"debug.json" + * @param {string} json - 需要写入调试文件的内容,默认为空数组 + * @returns {Promise} - 异步函数,没有返回值 + */ +async function debugKey(path = "debug.json", json = "", isText = false, key = dev.debug) { + const p = "debug\\" + // 检查是否处于调试模式 + if (dev.isDebug) { + if (!isText) { + log.warn("[{0}]正在写出{1}日志", '开发者模式', path) + // 将调试信息同步写入指定文件 + file.writeTextSync(`${p}${path}`, json) + log.warn("[{0}]写出完成", '开发者模式') + } else { + log.warn("[{0}]==>{1}", '开发者模式', json) + } + // 输出等待按键的提示信息 + log.warn("[{0}]请按下{1}继续执行", '开发者模式', key) + // 等待用户按下指定按键 + await keyMousePressStart(key) + } +} + + +/** + * 监听指定按键的按下和释放事件 + * @param {string} key - 需要监听的按键代码 + * @param {boolean} enableSkip - 是否允许跳过监听,默认为false + * @returns {Promise} 返回一个Promise对象,解析为包含按键状态的对象 + * - ok: boolean - 按键是否被完整按下并释放 + * - skip: boolean - 是否跳过监听(仅在enableSkip为true时有效) + */ +async function keyMousePress(key, enableSkip = false) { + let press = {ok: false, skip: false} // 初始化返回对象,记录按键状态 + const keyMouseHook = new KeyMouseHook() // 创建按键鼠标钩子实例 + let keyDown = false // 记录按键是否被按下 + let keyUp = false // 记录按键是否被释放 + let down = false // 记录按键按下事件是否触发 + let up = false // 记录按键释放事件是否触发 + try { + // 注册按键按下事件处理函数 + keyMouseHook.OnKeyDown(function (keyCode) { + log.debug("{keyCode}被按下", keyCode) + keyDown = (key === keyCode) // 检查是否是目标按键被按下 + down = true // 标记按键按下事件已触发 + }); + // 注册按键释放事件处理函数 + keyMouseHook.OnKeyUp(function (keyCode) { + log.debug("{keyCode}被释放", keyCode) + keyUp = (key === keyCode) // 检查是否是目标按键被释放 + up = true // 标记按键释放事件已触发 + }); + + // 循环等待直到按键被按下并释放,或者跳过条件满足 + while (true) { + if (enableSkip) { + if (press.ok || press.skip) { + break; + } + } else if (press.ok) { + break; + } + press.ok = keyDown && keyUp // , + press.skip = down && up // + await sleep(200) // 每次循环间隔200毫秒 + } + return press + } finally { + //脚本结束前,记得释放资源! + keyMouseHook.dispose() + } // 释放按键钩子资源 + +} + +/** + * 异步函数,用于检测特定按键的按下和释放事件 + * @param {string|number} key - 需要检测的按键代码 + * @returns {Promise} 返回一个Promise,解析为布尔值,表示是否检测到按键的完整按下和释放过程 + */ +async function keyMousePressStart(key) { + return (await keyMousePress(key)).ok +} + +/** + * 根据索引切换队伍 + * @param {number} index - 要切换的队伍在SevenElements数组中的索引 + * @returns {Promise} + */ +async function switchTeamByIndex(index, key) { + // 获取指定索引的队伍名称,如果索引超出范围或小于0则返回undefined + const teamName = team.SevenElements.length > index && index >= 0 ? team.SevenElements[index] : undefined; + // 检查队伍名称是否有效 + if (!teamName || teamName === "") { + // 如果没有设置队伍,记录调试日志并跳过切换 + log.debug(`[{mode}] 没有设置队伍: {teamName},跳过切换`, settings.mode, teamName); + } else if (team.current === teamName) { + // 如果当前已经是目标队伍,记录调试日志并跳过切换 + log.debug(`[{mode}] 当前队伍为: {teamName},无需切换`, settings.mode, teamName); + } else { + // 如果需要切换队伍,记录信息日志 + log.info(`[{mode}] 检测到需要: {key},切换至{val}`, settings.mode, key, teamName); + // 调用切换队伍的工具函数 + const teamSwitch = await switchUtil.SwitchPartyMain(teamName); + // 如果切换成功,更新当前队伍 + if (teamSwitch) { + team.current = teamSwitch; + } + } +} + +async function switchTeamByName(teamName) { + // 检查队伍名称是否有效 + if (!teamName || teamName === "") { + // 如果没有设置队伍,记录调试日志并跳过切换 + log.debug(`[{mode}] 没有设置队伍: {teamName},跳过切换`, settings.mode, teamName); + } else if (team.current === teamName) { + // 如果当前已经是目标队伍,记录调试日志并跳过切换 + log.debug(`[{mode}] 当前队伍为: {teamName},无需切换`, settings.mode, teamName); + } else { + // 如果需要切换队伍,记录信息日志 + log.info(`[{mode}] 切换至{val}`, settings.mode, teamName); + // 调用切换队伍的工具函数 + const teamSwitch = await switchUtil.SwitchPartyMain(teamName); + // 如果切换成功,更新当前队伍 + if (teamSwitch) { + team.current = teamSwitch; + } + } +} + +/** + * 执行指定路径的脚本文件 + * @param {string} path - 要执行的脚本路径 + */ +async function runPath(path, root_name = "", parent_name = "", current_name = "") { + // 参数验证 + if (!path || typeof path !== 'string') { + log.warn('无效的路径参数: {path}', path) + return + } + + // 检查该路径是否已经在执行中 + if (Record.paths.has(path)) { + log.info(`[{mode}] 路径已执行: {path},跳过执行`, settings.mode, path) + return + } + //检查战斗需求 + try { + if (!team.fight) { + //自动检测禁用 + // if (team.fightKeys.some(item => path.includes(`\\${item}\\`))) { + // team.fight = true + // } + } + } catch (error) { + log.error("检查战斗需求失败: {error}", error.message); + } + + //切换队伍 + const hoeGroundKey = `${parent_name}->${current_name}`; + const hoeGroundRootKey = `${root_name}->${parent_name}->${current_name}`; + if (team.HoeGroundMap.has(hoeGroundRootKey) || team.HoeGroundMap.has(hoeGroundKey)) { + const hoeGroundName = team.HoeGroundMap.get(hoeGroundKey) || team.HoeGroundMap.get(hoeGroundKey); + await switchTeamByName(hoeGroundName); + } else { + const entry = [...SevenElement.SevenElementsMap.entries()].find(([key, val]) => { + return val.some(item => path.includes(`\\${item}\\`)); + }); + if (entry) { + const [key, val] = entry; + const index = SevenElement.SevenElements.indexOf(key); + await switchTeamByIndex(index, `路线需要${key}元素`); + } else { + if (path.includes("有草神")) { + const idx = SevenElement.SevenElements.indexOf('草'); + await switchTeamByIndex(idx, "路线需要草神"); + }else if (team.current !== team.fightName) { + log.info(`[{mode}] 未检测到队伍配置切换至行走位,切换至{teamName}`,settings.mode, team.fightName); + const teamSwitch = await switchUtil.SwitchPartyMain(team.fightName); + if (teamSwitch) { + team.current = teamSwitch; + } + } + //自动检测已禁用 + else if (team.fight) { + if (!team.fightName) { + log.error(`[{mode}] 路径需要配置好战斗策略: {path}`, settings.mode, path) + throw new Error(`路径需要配置好战斗策略: ` + path) + } else if (team.current !== team.fightName) { + log.info(`[{mode}] 检测到需要战斗,切换至{teamName}`,settings.mode, team.fightName); + const teamSwitch = await switchUtil.SwitchPartyMain(team.fightName); + if (teamSwitch) { + team.current = teamSwitch; + } + } + } + } + } + //切换队伍-end + + log.info("开始执行路径: {path}", path) + await pathingScript.runFile(path) + try { + await sleep(1) + } catch (e) { + throw new Error(e.message) + } + if (team.fight) { + //启用战斗 + // await dispatcher.runAutoFightTask(new AutoFightParam()); + // await realTimeMissions(false) + // 重置战斗状态 + team.fight = false + } + log.debug("路径执行完成: {path}", path) + + if (auto.semi && auto.run) { + log.warn(`[{mode}] 路径执行完成: {path}, 请按{key}继续`, settings.mode, path, auto.key) + await keyMousePressStart(auto.key) + } +} + + +/** + * 执行给定的路径列表 + * @param {Array} list - 要执行的路径列表,默认为空数组 + * @returns {Promise} + */ +async function runList(list = [], key = "", current_name = "", parent_name = "", group_key = "", group_value = "") { + // 参数验证 + if (!Array.isArray(list)) { + log.warn('无效的路径列表参数: {list}', list); + return; + } + + if (list.length === 0) { + log.debug('路径列表为空,跳过执行'); + return; + } + log.info(`[{mode}] 开始执行 [{0}]组 路径列表,共{count}个路径`, settings.mode, key, list.length); + // 遍历路径列表 + for (let i = 0; i < list.length; i++) { + const onePath = list[i]; + const path = onePath.path; + if (i === 0) { + log.info(`[{mode}] 开始执行[{1}-{2}]列表`, settings.mode, parent_name, current_name); + } + log.info('任务组[{0}] ' + group_key + ',正在执行第{index}/{total}个路径: {path}', key, group_value, i + 1, list.length, path); + if (auto.semi && auto.skip) { + log.warn(`[{mode}] 按下{key}可跳过{0}执行,如不想跳过请按 空格 或 其他非功能键`, settings.mode, auto.key, path); + const skip = await keyMousePress(auto.key, auto.skip); + if (skip.skip) { + log.warn(`[{mode}] 按下{key}跳过{0}执行`, settings.mode, auto.key, path); + continue + } + } + const now = Date.now(); + const value = {timestamp: now, path: path}; + try { + // 执行单个路径,并传入停止标识 + await runPath(path, onePath.rootName, parent_name, current_name); + } catch (error) { + log.error('执行路径列表中的路径失败: {path}, 错误: {error}', path, error.message); + Record.errorPaths.add(path) + throw new Error(error.message) + // continue; // 继续执行列表中的下一个路径 + } + Record.paths.add(path) + RecordPath.paths.add(value) + } + + log.debug(`[{mode}] 路径列表执行完成`, settings.mode); +} + + +/** + * 遍历并执行Map中的任务 + * @param {Map} map - 包含任务信息的Map对象,默认为新的Map实例 + * @returns {Promise} - 异步执行,没有返回值 + */ +async function runMap(map = new Map()) { + // 参数验证 + if (!(map instanceof Map)) { + log.warn('无效的Map参数: {map}', map); + return; + } + + if (map.size === 0) { + log.debug('任务Map为空,跳过执行'); + return; + } + log.info(`========================================================`) + log.info(`[{mode}] 开始执行任务,共{count}组任务`, settings.mode, map.size); + //打印组执行顺序 + let index = 1 + for (const [key, one] of map.entries()) { + log.info(`[{mode}] 任务组[{0}] 执行顺序[{1}] 执行路径数[{2}]`, settings.mode, key, index + "/" + map.size, one?.paths?.length || 0); + index++ + } + const group_prefix = "任务组执行顺序[{group_key}]" + log.info(`========================================================`) + // 遍历Map中的所有键 + index = 0 + for (const [key, one] of map.entries()) { + index++ + if (one.paths.size <= 0) { + continue + } + const group = { + name: key, + paths: new Set(one.paths) + }; + try { + // 记录开始执行任务的日志信息 + log.debug(`[{0}] {1}组 开始执行...`, settings.mode, key); + // 执行当前任务关联的路径列表 + + await runList(one.paths, key, one.current_name, one.parent_name, group_prefix, (index + "/" + map.size)); + + log.debug(`[{0}] 任务[{1}]执行完成`, settings.mode, key); + } catch (error) { + log.error(`[{0}] 任务[{1}]执行失败: {error}`, settings.mode, key, error.message); + // continue; // 继续执行下一个任务 + throw new Error(error.message) + } + Record.groupPaths.add(group) + } + + log.debug(`[{mode}] 任务Map执行完成`, settings.mode); +} + +/** + * 获取上级文件夹名称(支持多级查找) + * @param {string} path - 完整路径 + * @param {number} level - 向上查找的层级,默认为1(即直接上级) + * @returns {string} 指定层级的文件夹名称,如果不存在则返回空字符串 + */ +function getParentFolderName(path, level = 1) { + if (!path || typeof path !== 'string' || level < 1) { + return undefined; + } + + // 统一处理路径分隔符 + const normalizedPath = path.replace(/\\/g, '/'); + + // 移除末尾的斜杠 + const trimmedPath = normalizedPath.replace(/\/$/, ''); + + // 按斜杠分割路径 + const pathParts = trimmedPath.split('/').filter(part => part !== ''); + + // 检查是否有足够的层级 + if (level >= pathParts.length) { + return undefined; + } + + // 返回指定层级的上级目录名称 + return pathParts[pathParts.length - level - 1]; +} + +/** + * 从根目录开始获取指定位置的文件夹名称(支持多级查找) + * @param {string} path - 完整路径 + * @param {number} level - 从根开始向下的层级,默认为1(即第一个子目录) + * @returns {string} 指定层级的文件夹名称,如果不存在则返回undefined + */ +function getChildFolderNameFromRoot(path, level = 1) { + if (!path || typeof path !== 'string' || level < 1) { + return undefined; + } + + // 统一处理路径分隔符 + const normalizedPath = path.replace(/\\/g, '/'); + + // 移除开头的斜杠(如果有) + const trimmedPath = normalizedPath.replace(/^\/+/, ''); + + // 按斜杠分割路径 + const pathParts = trimmedPath.split('/').filter(part => part !== ''); + + // 检查是否有足够的层级 + if (level > pathParts.length) { + return undefined; + } + + // 返回从根开始指定层级的目录名称(level - 1 是因为数组索引从0开始) + return pathParts[level - 1]; +} + +/** + * 按层级对列表项进行分组 + * @param {Array} list - 包含层级信息的列表项数组 + * @returns {Array} 返回一个嵌套数组,每个子数组包含对应层级的所有项 + */ +function groupByLevel(list) { + // 找出最大层级数 + const maxLevel = Math.max(...list.map(item => item.level)); + + // 创建嵌套数组结构 + const result = []; + + // 按层级分组 + for (let level = 0; level <= maxLevel; level++) { + const levelItems = list.filter(item => item.level === level); + result.push(levelItems); + } + + return result; +} + + +/** + * 递归读取指定路径下的文件和文件夹,构建树形结构 + * @param {string} path - 要读取的初始路径 + * @param {number} level - 当前层级(从 0 开始),默认为 0 + * @param {string} parentId - 父节点的 id(完整路径),根节点为 null + * @param {string[]} fullPathNames - 从根到当前节点的名称路径数组 + * @param {string} isFileKey - 目标文件类型的后缀名,默认为 ".json" + * @param {boolean} treeStructure - 是否返回树状结构(true: 树 / false: 只收集文件平铺) + * @returns {Promise} 树形或平铺的文件/文件夹结构数组 + */ +async function readPaths( + path, + level = 0, + parentId = null, + fullPathNames = [], + isFileKey = ".json", + treeStructure = true +) { + const treeList = []; + + // 获取当前路径下所有文件/文件夹 + let pathSyncList = file.readPathSync(path); + + if (!Array.isArray(pathSyncList)) { + pathSyncList = [...pathSyncList] + } + // log.error(JSON.stringify(...pathSyncList)) + // log.error(JSON.stringify(Array.isArray(pathSyncList))) + // + // 可选:按名称排序,保证同层顺序可预测(字母序或自定义规则) + pathSyncList.sort((a, b) => a.localeCompare(b)); + + for (const itemPath of pathSyncList) { + const currentName = getFileOrFolderName(itemPath); // 请确保你有这个辅助函数,取路径最后一段 + const currentId = itemPath; // 使用完整路径作为全局唯一 id(最可靠) + const currentFullPathNames = [...fullPathNames, currentName]; + + if (itemPath.endsWith(isFileKey)) { + // ── 是目标文件 ─────────────────────────────── + let displayName; + let parentDisplayName; + + // 你原有的复杂名称处理逻辑(可继续保留或简化) + const parentFolder = getParentFolderName(itemPath); + + if (!parentFolder) { + throw new Error(`${itemPath} 没有上级目录`); + } + + if (itemPath.includes("@")) { + displayName = getParentFolderName(itemPath, 2); + parentDisplayName = getParentFolderName(itemPath, 3); + } else { + displayName = parentFolder; + parentDisplayName = getParentFolderName(itemPath, 2); + } + + const fileNode = { + id: currentId, + name: displayName || currentName, + parentName: parentDisplayName, + parentId: parentId, + path: itemPath, + level: level + 1, + fullPathNames: currentFullPathNames, + levelName: "", + isRoot: false, + isFile: true, + children: [] // 文件没有子节点 + }; + + treeList.push(fileNode); + } else if (file.IsFolder(itemPath)) { + // ── 是文件夹 ─────────────────────────────── + const childNodes = await readPaths( + itemPath, + level + 1, + currentId, // 把当前路径作为子节点的 parentId + currentFullPathNames, // 传递路径栈 + isFileKey, + treeStructure + ); + + if (treeStructure) { + // 树形结构:保留文件夹节点 + const folderNode = { + id: currentId, + name: currentName, + parentName: fullPathNames.length > 0 ? fullPathNames[fullPathNames.length - 1] : undefined, + parentId: parentId, + path: itemPath, + level: level + 1, + fullPathNames: currentFullPathNames, + levelName: "", + isRoot: level === 0, + isFile: false, + children: childNodes + }; + treeList.push(folderNode); + } else { + // 非树形:只收集文件,文件夹本身不保留 + treeList.push(...childNodes); + } + } + } + + return treeList; +} + +// 辅助函数示例(请根据你的实际实现替换) +function getFileOrFolderName(fullPath) { + // 返回路径最后一段名称 + const parts = fullPath.split(/[\\/]/); + return parts[parts.length - 1]; +} + +function getParentFolderName(fullPath, upLevels = 1) { + // 返回上 N 级的文件夹名称 + const parts = fullPath.split(/[\\/]/).filter(Boolean); + if (parts.length <= upLevels) return undefined; + + const targetIndex = parts.length - 1 - upLevels; + return parts[targetIndex] || undefined; +} + +async function treeToList(treeList = []) { + let list = [] + for (const element of treeList) { + // 如果是文件,添加到结果列表 + // if (element.isFile) { + list.push(element); + // } + const child = element.children + if (child && child.length > 0) { + list.push(...await treeToList(child)) + // list = [...list, ...await treeToList(child)] + } + } + return list +} + diff --git a/repo/js/FullyAutoAndSemiAutoTools/manifest.json b/repo/js/FullyAutoAndSemiAutoTools/manifest.json new file mode 100644 index 000000000..ca8ef3f87 --- /dev/null +++ b/repo/js/FullyAutoAndSemiAutoTools/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 1, + "name": "全自动或半自动工具箱", + "version": "0.0.1", + "bgi_version": "0.54.3", + "key": "PGCSBY37NJA", + "description": "", + "saved_files": [], + "authors": [ + { + "name": "云端客", + "links": "https://github.com/Kirito520Asuna" + } + ], + "http_allowed_urls": [ + "https://*", + "http://*" + ], + "settings_ui": "settings.json", + "main": "main.js" +} \ No newline at end of file diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/F-md.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/F-md.jpg new file mode 100644 index 000000000..743fb56a3 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/F-md.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-auto.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-auto.jpg new file mode 100644 index 000000000..d6ff46cb7 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-auto.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-http.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-http.jpg new file mode 100644 index 000000000..f7603c608 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-http.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-re.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-re.jpg new file mode 100644 index 000000000..4597709a1 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-re.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-run-pathing-01.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-run-pathing-01.jpg new file mode 100644 index 000000000..e907ad78c Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-run-pathing-01.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-run-pathing.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-run-pathing.jpg new file mode 100644 index 000000000..1940711ff Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-run-pathing.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-team-map.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-team-map.jpg new file mode 100644 index 000000000..1ebe7d8d7 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-team-map.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/init-team.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/init-team.jpg new file mode 100644 index 000000000..77a3f3ab7 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/init-team.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/run-init-01.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/run-init-01.jpg new file mode 100644 index 000000000..df1691f70 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/run-init-01.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/md/run-init-cd-01.jpg b/repo/js/FullyAutoAndSemiAutoTools/md/run-init-cd-01.jpg new file mode 100644 index 000000000..8e8bb3f58 Binary files /dev/null and b/repo/js/FullyAutoAndSemiAutoTools/md/run-init-cd-01.jpg differ diff --git a/repo/js/FullyAutoAndSemiAutoTools/settings.json b/repo/js/FullyAutoAndSemiAutoTools/settings.json new file mode 100644 index 000000000..734227c89 --- /dev/null +++ b/repo/js/FullyAutoAndSemiAutoTools/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/utils/SwitchTeam.js b/repo/js/FullyAutoAndSemiAutoTools/utils/SwitchTeam.js new file mode 100644 index 000000000..5afa90a18 --- /dev/null +++ b/repo/js/FullyAutoAndSemiAutoTools/utils/SwitchTeam.js @@ -0,0 +1,211 @@ +// Party Setup +const QuickSetupButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Quick Setup Button.png"), 1100, 900, 400, 180); +const ConfigureTeamButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Configure Team Button.png"), 0, 900, 200, 180); +const ConfirmDeployButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Confirm Deploy Button.png"), 0, 900, 1920, 180); +// Slider +const LeftSliderTopRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Slider Top.png"), 650, 50, 100, 100); +const LeftSliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Slider Bottom.png"), 650, 100, 100, 900); +const MiddleSliderTopRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Slider Top.png"), 1250, 50, 100, 200); +const MiddleSliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Slider Bottom.png"), 1250, 100, 100, 900); +const RightSliderTopRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Slider Top.png"), 1750, 100, 100, 100); +const RightSliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Slider Bottom.png"), 1750, 100, 100, 900); + +// 翻页 +async function pageDown(SliderBottomRo) { + let captureRegion = captureGameRegion(); + let SliderBottom = captureRegion.find(SliderBottomRo); + captureRegion.dispose(); + if (SliderBottom.isExist()) { + log.info("当前页面已识别&点击完毕,向下滑动"); + // log.info("滑块当前位置:({x},{y},{h},{w})", SliderBottom.x, SliderBottom.y, SliderBottom.Width, SliderBottom.Height); + click(Math.ceil(SliderBottom.x + SliderBottom.Width / 2), Math.ceil(SliderBottom.y + SliderBottom.Height * 2)); + await moveMouseTo(0, 0); + await sleep(100); + } +} + +// 滑条顶端 +async function pageTop(SliderTopRo) { + let captureRegion = captureGameRegion(); + let SliderTop = captureRegion.find(SliderTopRo); + captureRegion.dispose(); + if (SliderTop.isExist()) { + log.info("识别到滑条顶端位置:({x},{y},{h},{w})", SliderTop.x, SliderTop.y, SliderTop.Width, SliderTop.Height); + await moveMouseTo(Math.ceil(SliderTop.x + SliderTop.Width / 2), Math.ceil(SliderTop.y + SliderTop.Height * 1)); + leftButtonDown(); + await sleep(1000); + leftButtonUp(); + await moveMouseTo(0, 0); + await sleep(100); + } +} + +// 切换队伍 +async function SwitchParty(partyName) { + let ConfigureStatue = false; + + let foundQuickSetup = false; + for (let j = 0; j < 2; j++) { // 尝试两次 + keyPress("VK_L"); + await sleep(2000); + for (let i = 0; i < 2; i++) { + let captureRegion = captureGameRegion(); + let QuickSetupButton = captureRegion.find(QuickSetupButtonRo); + captureRegion.dispose(); + if (QuickSetupButton.isExist()) { + log.info("已进入队伍配置页面"); + foundQuickSetup = true; + break; + } else { + await sleep(1000); + } + } + if (foundQuickSetup) { + break; // 第一次找到就退出循环 + } + } + + if (!foundQuickSetup) { + log.error("两次尝试都未能进入队伍配置页面"); + return false; + } + // 识别当前队伍 + let captureRegion = captureGameRegion(); + let resList = captureRegion.findMulti(RecognitionObject.ocr(100, 900, 300, 180)); + captureRegion.dispose(); + let currentPartyFound = false; + + for (let i = 0; i < resList.count; i++) { + let res = resList[i]; + log.info("当前队伍名称位置:({x},{y},{w},{h}), 识别结果:{text}", res.x, res.y, res.Width, res.Height, res.text); + if (res.text.includes(partyName)) { + log.info("当前队伍即为目标队伍,无需切换"); + notification.send(`当前队伍即为目标队伍:${partyName},无需切换`); + keyPress("VK_ESCAPE"); + await sleep(500); + currentPartyFound = true; + break; + } + } + if (!currentPartyFound) { + await sleep(1000); + let captureRegion = captureGameRegion(); + let ConfigureTeamButton = captureRegion.find(ConfigureTeamButtonRo); + captureRegion.dispose(); + if (ConfigureTeamButton.isExist()) { + log.info("识别到配置队伍按钮"); + ConfigureTeamButton.click(); + await sleep(500); + await pageTop(LeftSliderTopRo); + + for (let p = 0; p < 4; p++) { + // 识别当前页 + let captureRegion = captureGameRegion(); + let resList = captureRegion.findMulti(RecognitionObject.ocr(0, 100, 400, 900)); + captureRegion.dispose(); + for (let i = 0; i < resList.count; i++) { + let res = resList[i]; + if (settings.enableDebug) { + log.info("文本位置:({x},{y},{w},{h}), 识别内容:{text}", res.x, res.y, res.Width, res.Height, res.text); + } + if (res.text.includes(partyName)) { + log.info("目标队伍位置:({x},{y},{w},{h}), 识别结果:{text}", res.x, res.y, res.Width, res.Height, res.text); + click(Math.ceil(res.x + 360), res.y + Math.ceil(res.Height / 2)); + + // 找到目标队伍,点击确定、部署 + await sleep(1500); + let ConfirmButtonCaptureRegion = captureGameRegion(); + let ConfirmButton = ConfirmButtonCaptureRegion.find(ConfirmDeployButtonRo); + ConfirmButtonCaptureRegion.dispose(); + if (ConfirmButton.isExist()) { + log.info("识别到确定按钮:({x},{y},{w},{h})", ConfirmButton.x, ConfirmButton.y, ConfirmButton.Width, ConfirmButton.Height); + ConfirmButton.click(); + } + await sleep(1500); + let DeployButtonCaptureRegion = captureGameRegion(); + let DeployButton = DeployButtonCaptureRegion.find(ConfirmDeployButtonRo); + DeployButtonCaptureRegion.dispose(); + if (DeployButton.isExist()) { + log.info("识别到部署按钮:({x},{y},{w},{h})", DeployButton.x, DeployButton.y, DeployButton.Width, DeployButton.Height); + DeployButton.click(); + await sleep(100); + notification.send(`寻找到目标队伍:${partyName}`); + ConfigureStatue = true; + break; + } + } + } + if (ConfigureStatue) { + await genshin.returnMainUi(); + break; + } else { + await pageDown(LeftSliderBottomRo); + } + } + if (!ConfigureStatue) { + // 没找到指定队伍名称的队伍,抛出异常 + log.error(`没有找到指定队伍名称:${partyName}`); + notification.error(`没有找到指定队伍名称:${partyName}`); + await genshin.returnMainUi(); + throw new Error(`没有找到指定队伍名称:${partyName}`); + } + } else { + // 没找到配置队伍按钮,抛出异常 + log.error("没有找到配置队伍按钮"); + notification.error("没有找到配置队伍按钮"); + await genshin.returnMainUi(); + throw new Error("没有找到配置队伍按钮"); + } + } else { + // 当前队伍就是目标队伍,设置成功状态 + ConfigureStatue = true; + } + return ConfigureStatue; +} + +async function SwitchPartyMain(partyName, disableGoStatue) { + if (!!partyName) { + try { + if (!disableGoStatue) { + // 强制去七天神像换队 + log.info("强制传送到七天神像切换队伍"); + await genshin.TpToStatueOfTheSeven(); + log.info("正在尝试切换至" + partyName); + await SwitchParty(partyName); + } else { + // 先尝试在当前位置换队 + await genshin.returnMainUi(); + log.info("正在尝试切换至" + partyName); + let switchResult = await SwitchParty(partyName); + + if (!switchResult) { + // 如果当前位置换队失败,去七天神像再试一次 + log.info("当前位置换队失败,传送到七天神像重试"); + await genshin.TpToStatueOfTheSeven(); + log.info("正在七天神像重新尝试切换至" + partyName); + await SwitchParty(partyName); + } + } + genshin.clearPartyCache(); + return partyName + } catch (error) { + log.error("队伍切换失败:" + error.message); + notification.error("队伍切换失败:" + error.message); + await genshin.returnMainUi(); + } + } else { + log.error("没有设置切换队伍"); + notification.error("没有设置切换队伍"); + await genshin.returnMainUi(); + } +} +this.switchUtil={ + SwitchPartyMain +} +// /** +// * @returns {Promise} +// */ +// +// (async function () { +// await SwitchPartyMain(settings.partyName, settings.disableGoStatue); +// })(); \ No newline at end of file diff --git a/repo/js/FullyAutoAndSemiAutoTools/utils/cron.js b/repo/js/FullyAutoAndSemiAutoTools/utils/cron.js new file mode 100644 index 000000000..123fc8daa --- /dev/null +++ b/repo/js/FullyAutoAndSemiAutoTools/utils/cron.js @@ -0,0 +1,343 @@ +// 分钟 小时 日期 月份 星期 [年份可选] +const cronRegex = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+(\S+))?$/; + +/** + * 解析单个 cron 字段,返回匹配的数值数组 + * @param {string} field - 字段值 如 "1,3-5,* /10" + * @param {number} min - 最小值 + * @param {number} max - 最大值 + * @param {number} [stepBase=0] - 步进基准(星期从0或1开始都支持) + * @returns {Set} 所有匹配的数值集合 + */ +function parseCronField(field, min, max, stepBase = 0) { + const result = new Set(); + const parts = field.split(','); + + for (const part of parts) { + // 处理 */n 格式 + if (part.startsWith('*/')) { + const step = parseInt(part.slice(2), 10); + if (isNaN(step) || step <= 0) continue; + + for (let i = min; i <= max; i += step) { + result.add(i); + } + continue; + } + + // 处理 n-n/n 格式 + if (part.includes('/')) { + const [range, stepStr] = part.split('/'); + const step = parseInt(stepStr, 10); + if (isNaN(step) || step <= 0) continue; + + if (range === '*') { + for (let i = min; i <= max; i += step) { + result.add(i); + } + } else if (range.includes('-')) { + const [start, end] = range.split('-').map(Number); + if (isNaN(start) || isNaN(end)) continue; + for (let i = Math.max(min, start); i <= Math.min(max, end); i += step) { + result.add(i); + } + } + continue; + } + + // 处理范围 n-n + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.max(min, start); i <= Math.min(max, end); i++) { + result.add(i); + } + } + continue; + } + + // 处理单个数字 / 列表 + const num = parseInt(part, 10); + if (!isNaN(num) && num >= min && num <= max) { + result.add(num); + } + + // 处理 L(最后一天) - 只对日期字段有意义,这里简单处理 + if (part === 'L' && max === 31) { + // 实际使用时需要知道当月天数,这里先占位 + result.add(99); // 特殊标记,后续需处理 + } + + // ? 在日期/星期中代表「无特定值」 - 这里简单忽略具体值 + if (part === '?') { + // 实际使用时需配合另一字段判断 + } + } + + return result; +} + +/** + * 简单版 cron 解析器(只解析出每个字段允许的时间值) + * @param {string} cron - cron表达式 "0 9 * * 1-5" + * @returns {object|null} 解析结果 或 null(格式错误) + */ +function parseCron(cron) { + const match = cron.trim().match(cronRegex); + if (!match) return null; + + const [, min, hour, day, month, dow] = match; + + try { + const minutes = parseCronField(min, 0, 59); + const hours = parseCronField(hour, 0, 23); + const days = parseCronField(day, 1, 31); + const months = parseCronField(month, 1, 12); + const dows = parseCronField(dow, 0, 7); // 0和7都代表周日 + + // 星期字段 7 → 0 + if (dows.has(7)) dows.add(0); + + return { + minutes: [...minutes].sort((a, b) => a - b), + hours: [...hours].sort((a, b) => a - b), + days: [...days].sort((a, b) => a - b), + months: [...months].sort((a, b) => a - b), + dows: [...dows].sort((a, b) => a - b), + // 注:days和dows同时有特定值时,实际cron是「或」关系 + // 这里仅简单分开列出,真实调度需复杂判断 + original: cron.trim(), + isValid: true + }; + } catch (e) { + return {isValid: false, error: e.message, original: cron}; + } +} + + +/** + * 获取下一个Cron时间戳 + * @param {string} cronExpression - Cron表达式 + * @param {number} [startTimestamp=Date.now()] - 开始时间戳,默认为当前时间 + * @param {number} endTimestamp - 结束时间戳 + * @returns {Promise} 返回一个Promise,解析为下一个Cron时间戳 + */ +async function getNextCronTimestamp(cronExpression, startTimestamp = Date.now(), endTimestamp, url) { + log.warn("使用cron CD算法 请开启JS HTTP 权限 如已开启请忽略") + const result = await http.request("POST", url, JSON.stringify({ + cronExpression: `${cronExpression}`, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + }), JSON.stringify({ + "Content-Type": "application/json" + })).then(res => { + log.debug(`[{0}]res=>{1}`, 'next', JSON.stringify(res)) + if (res.status_code === 200 && res.body) { + let result_json = JSON.parse(res.body); + if (result_json?.code === 200) { + return result_json?.data + } + throw new Error("请求失败,error:" + result_json?.message) + } + return undefined + }) + + return result === null || !result ? undefined : result +} + +/** + * 获取所有cron表达式的时间戳 + * @param {Array} bodyJson - 包含cron表达式和相关参数的对象数组,默认值为空数组 + * @param {string} url - 请求的目标URL + * @returns {Map} 返回一个Map对象,其中key为输入参数中的key,value为对应的ok状态 + */ +async function getNextCronTimestampAll(bodyJson = [{ + key: '', // 标识符 + cronExpression: "", // cron表达式 + startTimestamp: Date.now(), // 开始时间戳 + endTimestamp: 0 // 结束时间戳 +}], url) { + // 打印警告日志,提示用户需要开启JS HTTP权限 + log.warn("使用cron CD算法 请开启JS HTTP 权限 如已开启请忽略") + // 初始化结果列表,默认包含一个成功的空key项 + let resultList = [{key: '', ok: true,next:0}] + // 发送HTTP POST请求并处理响应 + resultList = ( + await http.request("POST", url, JSON.stringify({cronList:bodyJson}), JSON.stringify({ + "Content-Type": "application/json" + })).then(res => { + // 打印调试日志,记录响应内容 + log.debug(`[{0}]res=>{1}`, 'next', JSON.stringify(res)) + // 检查响应状态码和响应体 + if (res.status_code === 200 && res.body) { + let result_json = JSON.parse(res.body); + // 检查响应代码是否为200 + if (result_json?.code === 200) { + return result_json?.data + } + // 如果响应代码不为200,抛出错误 + throw new Error("请求失败,error:" + result_json?.message) + } + return undefined + }) + ) + // 创建一个Map对象,用于存储结果 + let map = new Map() + // 遍历结果列表,将每个项的key和ok状态存入Map + resultList.forEach(item => { + map.set(item.key, item.ok) + }) + + // 返回包含结果的Map对象 + return map +} + +//影响到性能 改http 第三方 +// function getNextCronTimestamp(cron, fromTime = Date.now(),endTime) { +// const parts = cron.trim().split(/\s+/); +// if (parts.length < 5 || parts.length > 6) { +// throw new Error("不支持的 cron 格式,应为 5~6 段"); +// } +// +// const [minStr, hourStr, dayStr, monthStr, dowStr] = parts; +// +// // 解析每个字段 +// const minutes = parseField(minStr, 0, 59); +// const hours = parseField(hourStr, 0, 23); +// const days = parseField(dayStr, 1, 31); +// const months = parseField(monthStr, 1, 12); +// const dows = parseField(dowStr, 0, 7); +// +// // 星期 7 → 0 (周日) +// if (dows.has(7)) dows.add(0); +// +// let current = new Date(fromTime); +// // 如果没有指定 endTime,默认设置为明天 00:00:00 +// if (endTime === undefined) { +// const tomorrow = new Date(current); +// tomorrow.setDate(tomorrow.getDate() + 1); +// tomorrow.setHours(0, 0, 0, 0); +// endTime = tomorrow.getTime(); +// } +// +// // 动态计算最大迭代次数 +// // 将时间差(毫秒)转换为分钟,向上取整,确保覆盖所有可能的分钟点 +// const timeDiffMinutes = Math.ceil((endTime - fromTime) / 60000); +// +// // 设置最大迭代次数,防止意外情况(如 endTime 极大)导致内存溢出或死循环 +// // 即使 endTime 是 10 年后,也限制在约 2 年内(避免极端情况) +// // 2年 ≈ 365 * 2 * 24 * 60 = 1,051,200 +// const MAX_ITERATIONS_LIMIT = 1051200; +// const MAX_ITERATIONS = Math.min(timeDiffMinutes, MAX_ITERATIONS_LIMIT); +// +// let iteration = 0; +// while (iteration++ < MAX_ITERATIONS) { +// // 先推进到下一分钟,避免死循环在同一分钟 +// current.setMinutes(current.getMinutes() + 1); +// current.setSeconds(0); +// current.setMilliseconds(0); +// +// const m = current.getMinutes(); +// const h = current.getHours(); +// const d = current.getDate(); +// const mon = current.getMonth() + 1; // JS 月份 0~11 +// const dow = current.getDay(); // 0=周日, 1=周一, ..., 6=周六 +// +// // 核心匹配条件(日期和星期是 OR 关系) +// const minuteMatch = minutes.has(m) || minutes.size === 0; +// const hourMatch = hours.has(h) || hours.size === 0; +// const monthMatch = months.has(mon) || months.size === 0; +// const dayMatch = days.has(d) || days.size === 0; +// const dowMatch = dows.has(dow) || dows.size === 0; +// +// const dateOrDowMatch = (days.size === 0 && dows.size === 0) || // 两者都是 * +// (days.size > 0 && dows.size === 0) || // 只指定了日期 +// (days.size === 0 && dows.size > 0) || // 只指定了星期 +// (dayMatch && dowMatch); // 两者都满足才算(最严格) +// +// if (minuteMatch && hourMatch && monthMatch && dateOrDowMatch) { +// return current.getTime(); +// } +// } +// +// // 如果是因为超过 MAX_ITERATIONS_LIMIT 而退出,说明时间跨度太大 +// if (timeDiffMinutes > MAX_ITERATIONS_LIMIT) { +// log.warn("查找范围过大,已达到最大迭代次数限制"); +// } else { +// log.warn("未找到合理下一次执行时间"); +// } +// return null; +// } + +/** + * 解析单个 cron 字段,返回匹配的数值 Set + * 支持: * , - / 数值列表 * /n + */ +function parseField(field, min, max) { + const result = new Set(); + + if (field === '*' || field === '?') { + return result; // 空 set 代表任意 + } + + const parts = field.split(','); + + for (let part of parts) { + part = part.trim(); + + // */n + if (part.startsWith('*/')) { + const step = Number(part.slice(2)); + if (!isNaN(step) && step > 0) { + for (let i = min; i <= max; i += step) { + result.add(i); + } + } + continue; + } + + // n-n + if (part.includes('-') && !part.includes('/')) { + const [start, end] = part.split('-').map(Number); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.max(min, start); i <= Math.min(max, end); i++) { + result.add(i); + } + } + continue; + } + + // n/n 或 */n 已处理,剩下是普通数字或范围/步进 + if (part.includes('/')) { + const [range, stepStr] = part.split('/'); + const step = Number(stepStr); + if (isNaN(step) || step <= 0) continue; + + if (range === '*') { + for (let i = min; i <= max; i += step) result.add(i); + } else { + const [start, end] = range.split('-').map(Number); + if (!isNaN(start) && !isNaN(end)) { + for (let i = Math.max(min, start); i <= Math.min(max, end); i += step) { + result.add(i); + } + } + } + continue; + } + + // 单个数字 + const num = Number(part); + if (!isNaN(num) && num >= min && num <= max) { + result.add(num); + } + } + + return result; +} + + +this.cronUtil = { + getNextCronTimestamp, + getNextCronTimestampAll +} \ No newline at end of file diff --git a/repo/js/FullyAutoAndSemiAutoTools/utils/uid.js b/repo/js/FullyAutoAndSemiAutoTools/utils/uid.js new file mode 100644 index 000000000..9bd0a118c --- /dev/null +++ b/repo/js/FullyAutoAndSemiAutoTools/utils/uid.js @@ -0,0 +1,142 @@ +const commonPath = 'Assets/RecognitionObject/' +const commonMap = new Map([ + ['main_ui', { + path: `${commonPath}`, + name: 'paimon_menu', + type: '.png', + }], +]) +const genshinJson = { + width: 1920,//genshin.width, + height: 1080,//genshin.height, +} + +/** + * 根据键值获取JSON路径 + * @param {string} key - 要查找的键值 + * @returns {any} 返回与键值对应的JSON路径值 + */ +function getJsonPath(key) { + return commonMap.get(key); // 通过commonMap的get方法获取指定键对应的值 +} + +function saveOnlyNumber(str) { + str = str ? str : ''; + // 使用正则表达式匹配字符串中的所有数字 + // \d+ 匹配一个或多个数字 + // .join('') 将匹配到的数字数组连接成一个字符串 + // parseInt 将连接后的字符串转换为整数 + return parseInt(str.match(/\d+/g).join('')); +} + +async function ocrUID() { + let uid_json = { + x: 1683, + y: 1051, + width: 234, + height: 28, + } + let recognitionObjectOcr = RecognitionObject.Ocr(uid_json.x, uid_json.y, uid_json.width, uid_json.height); + let region3 = captureGameRegion() + let res = region3.find(recognitionObjectOcr); + log.debug(`[OCR识别UID]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`); + //只保留数字 + let uid + try { + uid = saveOnlyNumber(res.text) + } catch (e) { + log.warn(`UID未设置`) + uid = 0 + } finally { + region3.dispose() + } + log.debug(`[OCR识别UID]识别结果: {uid}`, uid); + return uid +} + +// 判断是否在主界面的函数 +const isInMainUI = () => { + // let name = '主界面' + let main_ui = getJsonPath('main_ui'); + // 定义识别对象 + let paimonMenuRo = RecognitionObject.TemplateMatch( + file.ReadImageMatSync(`${main_ui.path}${main_ui.name}${main_ui.type}`), + 0, + 0, + genshinJson.width / 3.0, + genshinJson.width / 5.0 + ); + let captureRegion = captureGameRegion(); + let res = captureRegion.find(paimonMenuRo); + captureRegion.Dispose() + return !res.isEmpty(); +}; + +async function toMainUi() { + let ms = 1000 + let index = 1 + await sleep(ms); + while (!isInMainUI()) { + await sleep(ms); + await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面 + await sleep(ms); + if (index > 3) { + throw new Error(`多次尝试返回主界面失败`); + } + index += 1 + } + +} + +async function compareUid(UID = settings.uid) { + let uid = await ocrUID() + let setUid = 0 + try { + setUid = saveOnlyNumber(UID) + } catch (e) { + // log.warn(`UID未设置`) + } + let compare = uid === setUid + if (compare) { + log.info(`[OCR识别UID]识别结果: {uid} 与设置UID相同`, uid); + } + return compare +} + +async function checkUid() { + let reJson = { + inMainUI: false, + isUid: false + } + if (isInMainUI()) { + reJson.isUid = await compareUid() + } + return reJson +} + +async function check() { + let check = false + if (settings.uid) { + try { + await toMainUi(); + } catch (e) { + log.warn("多次尝试返回主界面失败") + } + let checkJson = await checkUid() + if ((!checkJson.inMainUI) && (!checkJson.isUid)) { + //尝试直接识别 + checkJson.isUid = await compareUid() + } + check = checkJson.isUid + } + return check +} + +this.uidUtil = { + toMainUi, + isInMainUI, + checkUid, + ocrUID, + check, + compareUid, +} \ No newline at end of file