diff --git a/repo/js/AutoPlan/README.md b/repo/js/AutoPlan/README.md new file mode 100644 index 000000000..307397102 --- /dev/null +++ b/repo/js/AutoPlan/README.md @@ -0,0 +1,267 @@ +# AutoPlan自动体力计划 + +**自动体力计划JS** +本脚本通过调用 **BetterGI** 本体自动秘境实现体力计划 + +## 功能说明 + +本脚本为 BetterGI 的自动秘境功能提供**计划/排期**能力,让你能: + +- 为不同队伍配置不同的秘境刷取目标 +- 设置刷取轮次 +- 限制只在特定星期几执行(例如周日限定本、周本) +- 设置执行优先级(数字越大越先跑) +- 支持三种配置来源(输入 / UID专属 / bgi_tools远程) + +## 配置项说明 + +脚本通过以下界面配置项进行设置(对应 settings.json): + +| 配置项 | 类型 | 说明 | 示例值 | +|-----------------------------------------|----------------|-------------------------------------------------------------------------------------------------------------|------------------------------------------| +| **key** | input-text | 版本密钥(必填,仔细看本文档) | xxxx | +| **auto_load** | multi-checkbox | 加载模式(可多选)
・输入加载:使用下方 run_config 字段
・UID加载:读取 config/run_config.json(每个UID独立)
・bgi_tools加载:从远程API拉取 | 默认:["输入加载","UID加载"] | +| **run_config** | input-text | 自动秘境计划配置(当选择“输入加载”时生效)
格式见下方“计划配置语法” | 速刷\|苍白的遗荣\|3\|1-3\|0,6\|9
(多条用英文逗号分隔) | +| **loop_plan** | checkbox | 启用循环体力计划 | | +| **retry_count** | select | 复活重试次数 | | +| **bgi_tools_http_pull_json_config** | input-text | bgi_tools 拉取配置的 API 地址(当选择 bgi_tools加载 时生效) | https://example.com/api/pull | +| **bgi_tools_open_push** | checkbox | 是否在脚本结束时推送当前全部配置给 bgi_tools(用于同步/备份) | 勾选 = 开启推送 | +| **bgi_tools_http_push_all_json_config** | input-text | bgi_tools 推送全部配置的 API 地址(当开启推送时使用) | https://example.com/api/push-all | +| **bgi_tools_token** | input-text | bgi_tools授权token 语法:tokenName=tokenValue | tokenName=tokenValue | + +### 计划配置语法(run_config 字段) + +``` +队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日(1-3和本体的一致)|周几执行(0-6)不填默认执行|树脂使用顺序|执行顺序(越大越先执行) +``` + +- 多条计划用 **英文半角逗号 ,** 分隔 +- 字段之间用 **英文半角竖线 |** 分隔 +- **必须** 填写:秘境名称/物品名称 +- 其他字段可省略(留空即可,但竖线不能省) + +**字段详解** + +| 位置 | 字段 | 是否必填 | 说明 | 示例 | +|:--:|:-----------:|:------:|:--------------------------------------:|:-----------------------:| +| 1 | 类型 | **必填** | 秘境/地脉(后期支持地脉冗余字段) | 秘境/地脉 | +| 2 | 周几执行(0-6) | 可选 | 0=周日,1=周一,...,6=周六;可多选按`/`分割,不填=每天都可执行 | 0/3,3 | +| 3 | 执行顺序 | 可选 | 数字越大越优先执行(同时间点先跑优先级高的) | 9 / 5 / 1 | +| | | | ***`秘境类型后几位参数说明`*** +| 4 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称(用于切换队伍) | 速刷 / 雷国 / 国家队 | +| 5 | 秘境名称/刷取物品名称 | **必填** | 与 BetterGI 自动秘境识别的名称保持一致 | 苍白的遗荣 / 炽烈的炎之魔女 / 辰砂往生录 | +| 6 | 刷几轮 | 可选 | 整数,执行几轮(每轮 = 1次完整秘境) | 3 / 5 / 10 | +| 7 | 限时/周日 | 可选 | 和本体的1-3一致 | 1 / 2 / 3 | +| 8 | 树脂使用顺序 | 可选 | 原粹树脂,浓缩树脂,须臾树脂,脆弱树脂`/`分割,不填=默认执行 原粹树脂 | 原粹树脂/浓缩树脂,须臾树脂 | +| | | | ***`地脉类型后几位参数说明`*** +| 4 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称(用于切换队伍) | 速刷 / 雷国 / 国家队 | +| 5 | 国家 | **必填** | 识别国家(用于切换国家) | 纳塔 ... | +| 6 | 刷几轮 | **必填** | 刷几轮 | 1 | +| 7 | 地脉类型 | **必填** | 启示之花/藏金之花 | 启示之花 / 藏金之花 | +| 8 | 好感队 | 可选 | | | +| 9 | 使用脆弱树脂 | 可选 | 启用随便填个值 | +| 10 | 使用须臾树脂 | 可选 | 启用随便填个值 | +| 11 | 合成浓缩树脂 | 可选 | 启用随便填个值 | +| 12 | 使用冒险家之书 | 可选 | 启用随便填个值 | +| 13 | 详细通知 | 可选 | 启用随便填个值 | +| 14 | 战斗超时 | 可选 | 可选,默认 120 | + +**配置示例** + +``` +秘境|0/3|9|速刷|苍白的遗荣|3|1, # 优先级最高,周日,周三刷3轮遗荣 +秘境||5|国家队|炽烈的炎之魔女|5||, # 优先级次之,每天刷5轮魔女 +秘境|0|2|雷国|无想之刃狭间|2|2|浓缩树脂/原粹树脂, # 只在周日刷,优先使用浓缩树脂后使用原粹树脂,优先级较低 +地脉||1||蒙德|1|启示之花|||||||120 #优先级较低 刷1轮蒙德经验书 +``` + +(注意:最后一条也可以不带逗号) + +### 计划配置语法(config/run_config.json 配置) + +```json +[ + [ + "uid", + [ + { + "order": 1, + // 顺序值 + "days": [ + 0 + ], + // 执行日期 + "runType": "秘境", + // 预留地脉 类型支持。 秘境、地脉 + "autoFight": { + "physical": [ + { + "order": 0, + "name": "原粹树脂", + "open": true + }, + { + "order": 1, + "name": "浓缩树脂", + "open": false + }, + { + "order": 2, + "name": "须臾树脂", + "open": false + }, + { + "order": 3, + "name": "脆弱树脂", + "open": false + } + ], + //树脂开启和使用顺序 + "domainName": undefined, + //秘境名称 + "partyName": undefined, + //队伍名称 + "sundaySelectedValue": undefined, + //周日|限时选择的值 + "domainRoundNum": undefined//副本轮数 + } + // 秘境信息对象, + "autoLeyLineOutcrop": { + "count": 0, + //刷几轮 + "country": "", + //国家 + "leyLineOutcropType": "启示之花", + //地脉类型 启示之花/藏金之花 + "useAdventurerHandbook": false, + //使用冒险家之书 + "friendshipTeam": "", + //好感队伍名称 + "team": "", + //队伍名称 + "timeout": 120, + //战斗超时 + "isGoToSynthesizer": false, + //合成浓缩树脂 + "useFragileResin": false, + //使用脆弱树脂 + "useTransientResin": false, + //使用须臾树脂 + "isNotification": false + //详细通知 + } + //地脉信息对象 + } + ] + ] +] +``` + +```json +[ + [ + "10000002", + [ + { + "order": 1, + "days": [ + 0 + ], + "runType": "秘境", + "autoFight": { + "physical": [ + { + "order": 0, + "name": "原粹树脂", + "open": true + }, + { + "order": 1, + "name": "浓缩树脂", + "open": false + }, + { + "order": 2, + "name": "须臾树脂", + "open": false + }, + { + "order": 3, + "name": "脆弱树脂", + "open": false + } + ], + "domainName": "", + "partyName": "", + "sundaySelectedValue": 1, + "domainRoundNum": 1 + } + }, + { + "order": 2, + "days": [], + "runType": "地脉", + "autoLeyLineOutcrop": { + "count": 1, + "country": "纳塔", + "leyLineOutcropType": "启示之花", + "useAdventurerHandbook": false, + "friendshipTeam": "", + "team": "", + "timeout": 120, + "isGoToSynthesizer": false, + "useFragileResin": false, + "useTransientResin": false, + "isNotification": false + } + } + ] + ] +] +``` + +### 如果你不想研究语法 请部署[bettergi-scripts-tools](https://github.com/Kirito520Asuna/bettergi-scripts-tools) v0.0.4以上版本 + +***`话不多说直接上图:`*** + +![login](md/bgi-tools-login.jpg) +![init](md/bgi-tools-config-ui-init.jpg) +![config](md/bgi-tools-config-ui.jpg) +![config-01](md/bgi-tools-config-ui-01.jpg) +![config-02](md/bgi-tools-config-ui-02.jpg) + +## 使用建议 + +1. 第一次使用建议只勾选「输入加载」,把计划写在 `run_config` 里测试 +2. 熟练后可改为「UID加载」,把配置保存为 `config/run_config.json`(每个账号独立) +3. 如果你有自己的云端配置服务,可使用「bgi_tools加载」 + 推送功能实现多端同步 +4. 确保 BetterGI 的**自动秘境**功能已正常可用(钟离识别、战斗策略等) + +## 常见问题 + +- 计划没有执行? + → 检查密钥是否正确、是否勾选了对应的加载模式、秘境名称是否与 BetterGI 完全一致 + +- 周几限制不生效? + → 确认 BetterGI 系统时间正确,且格式为 0-6(0=周日) + +欢迎提交 issue 或 PR 改进脚本~ +祝刷本愉快! + +## 版本密钥 + +| 版本 | 密钥 | +|-------|---------------------| +| 0.0.1 | oiJbmjU2R0NniiwiZxh | + +## 版本历史(简要) + +### 0.0.1 2026.01.30 + +- 基本功能完成 +- 支持三种配置加载方式 +- 支持 bgi_tools http 拉取/推送配置 +- 支持按队伍、秘境、轮次、周几、优先级执行计划 + +**作者**:云端客 (Kirito520Asuna) + diff --git a/repo/js/AutoPlan/assets/200.png b/repo/js/AutoPlan/assets/200.png new file mode 100644 index 000000000..5fdf7d70b Binary files /dev/null and b/repo/js/AutoPlan/assets/200.png differ diff --git a/repo/js/AutoPlan/assets/add_button.jpg b/repo/js/AutoPlan/assets/add_button.jpg new file mode 100644 index 000000000..cf344bf81 Binary files /dev/null and b/repo/js/AutoPlan/assets/add_button.jpg differ diff --git a/repo/js/AutoPlan/assets/out_domain.jpg b/repo/js/AutoPlan/assets/out_domain.jpg new file mode 100644 index 000000000..d082b9269 Binary files /dev/null and b/repo/js/AutoPlan/assets/out_domain.jpg differ diff --git a/repo/js/AutoPlan/assets/paimon_menu.png b/repo/js/AutoPlan/assets/paimon_menu.png new file mode 100644 index 000000000..c424325b1 Binary files /dev/null and b/repo/js/AutoPlan/assets/paimon_menu.png differ diff --git a/repo/js/AutoPlan/assets/yue.png b/repo/js/AutoPlan/assets/yue.png new file mode 100644 index 000000000..dfa75916a Binary files /dev/null and b/repo/js/AutoPlan/assets/yue.png differ diff --git a/repo/js/AutoPlan/config/config.js b/repo/js/AutoPlan/config/config.js new file mode 100644 index 000000000..9512d3084 --- /dev/null +++ b/repo/js/AutoPlan/config/config.js @@ -0,0 +1,279 @@ +import {ocrUid} from "../utils/uid"; + +const config = { + //setting设置放在这个json + run: { + loop_plan: false,//启用循环体力计划 + retry_count: 3,//复活重试次数 + config: '', + // load_uid_config: false, + loads: [],//加载方式list + }, + bgi_tools: { + //授权token + token: { + name: 'Authorization', + value: '' + }, + api: { + httpPullJsonConfig: undefined, + httpPushAllJsonConfig: undefined, + httpPushAllCountryConfig: undefined, + }, + open: {open_push: false} + }, + info: { + key: undefined,//密钥 + manifest: {}, + settings: undefined + }, + user: { + uid: undefined, + physical: { + min: 20,//最小体力 + current: 0,//当前体力 + names: ["原粹树脂", "浓缩树脂", "须臾树脂", "脆弱树脂"] + }, + runTypes: ['秘境', '地脉'] + }, + // + path: { + manifest: "manifest.json", + domain: "config/domain.json", + runConfig: "config/run_config.json", + countryList: "config/countryList.json" + }, + //所有秘境信息 + domainList: [], + //所有秘境名称 + domainNames: new Set(), + //物品名称(只记录顶级的名称->金色物品名称) + itemNames: new Set(), + //秘境名称映射物品列表 + domainMap: new Map(), + //秘境名称映射秘境列表顺序 + domainOrderMap: new Map(), + //物品名称映射秘境名称 + domainItemsMap: new Map(), +} + +const LoadType = Object.freeze({ + uid: 'uid',//uid加载 + input: 'input',//input加载 + bgi_tools: 'bgi_tools',//bgi_tools加载 + fromValue(value) { + return Object.keys(this).find(key => this[key] === value); + } +}) +const LoadMap = new Map([ + ['UID加载', LoadType.uid], + ['输入加载', LoadType.input], + ['bgi_tools加载', LoadType.bgi_tools], +]) + +/** + * 初始化设置函数 + * 从配置文件中读取设置信息并返回 + * @returns {Object} 返回解析后的JSON设置对象 + */ +async function initSettings() { + // 默认设置文件路径 + let settings_ui = "settings.json"; + try { + // 读取并解析manifest.json文件 + config.info.manifest = JSON.parse(file.readTextSync(config.path.manifest)); + // 调试日志:输出manifest内容 + log.debug("manifest={key}", config.info.manifest); + // 调试日志:输出manifest中的settings_ui配置 + log.debug("settings_ui={key}", config.info.manifest.settings_ui); + log.info(`|脚本名称:{name},版本:{version}`, config.info.manifest.name, config.info.manifest.version); + if (config.info.manifest.bgi_version) { + log.info(`|最小可执行BGI版本:{bgi_version}`, config.info.manifest.bgi_version); + } + log.info(`|脚本作者:{authors}\n`, config.info.manifest.authors.map(a => a.name).join(",")); + // 更新settings_ui变量为manifest中指定的路径 + settings_ui = config.info.manifest.settings_ui + } catch (error) { + // 捕获并记录可能的错误 + log.warn("{error}", error.message); + } + // 读取并解析设置文件 + const settingsJson = JSON.parse(file.readTextSync(settings_ui)); + // 如果configSettings未定义,则将其设置为解析后的设置对象 + if (!config.info.settings) { + config.info.settings = settingsJson + } + // 调试日志:输出最终解析的设置对象 + log.debug("settingsJson={key}", settingsJson); + // 返回设置对象 + return settingsJson +} + +/** + * 获取多复选框的映射表 + * 该函数会从初始化的设置中提取所有类型为"multi-checkbox"的条目, + * 并将这些条目的名称和对应的选项值存储在一个Map对象中返回 + * @returns {Promise} 返回一个Promise对象,解析为包含多复选框配置的Map + */ +async function getMultiCheckboxMap() { + // 如果configSettings存在则使用它,否则调用initSettings()函数获取 + const settingsJson = config.info.settings ? config.info.settings : await initSettings(); + // 创建一个新的Map对象用于存储多复选框的配置 + // Map结构为: {名称: 选项数组} + let multiCheckboxMap = new Map(); + // 遍历设置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]) : []; + // 记录调试信息,包含名称、标签、选项和选项数量 + log.debug("name={key1},label={key2},options={key3},length={key4}", name, label, JSON.stringify(options), options.length); + // 将名称和对应的选项数组存入Map + multiCheckboxMap.set(name, options); + }) + // 返回包含多复选框配置的Map + return multiCheckboxMap +} + +/** + * 根据复选框组名称获取对应的值 + * 这是一个异步函数,用于从复选框映射中获取指定名称的值 + * @param {string} name - 复选框组的名称 + * @returns {Promise} 返回一个Promise,解析为复选框组对应的值 + */ +async function getValueByMultiCheckboxName(name) { + // 获取复选框映射表,这是一个异步操作 + let multiCheckboxMap = await getMultiCheckboxMap() + // log.debug("multiCheckboxMap={key}", JSON.stringify(multiCheckboxMap)) + // multiCheckboxMap.entries().forEach(([name, options]) => { + // log.debug("name={key1},options={key2}", name, JSON.stringify(options)) + // }) + // 从映射表中获取并返回指定名称对应的值 + let values = multiCheckboxMap.get(name); + log.debug("values={key}", JSON.stringify(values)) + return values +} + +/** + * 检查密钥是否正确 + */ +async function checkKey(key = "") { + if (config?.info?.manifest?.key !== key?.trim()) { + throw new Error("密钥错误"); + } +} + +/** + * 初始化秘境配置 + * @returns {Promise} + */ +async function initConfig() { + config.info.key = settings.key || config.info.key + await checkKey(config.info.key) + // //流程->返回主页 打开地图 返回主页 + // const physical = await ocrPhysical(true, true) + // config.user.physical.current = physical.current + // config.user.physical.min = physical.min + // 初始化uid + config.user.uid = await ocrUid() + // config.run.retry_count = (settings.retry_count ? parseInt(settings.retry_count) : config.run.retry_count) + + const retryCount = Number.parseInt(String(settings.retry_count ?? ""), 10); + config.run.retry_count = Number.isFinite(retryCount) && retryCount > 0 + ? retryCount + : config.run.retry_count; + + config.run.loop_plan = settings.loop_plan !== undefined ? settings.loop_plan : config.run.loop_plan + const bgi_tools_token = settings.bgi_tools_token || "Authorization= " + // const list = Array.from(bgi_tools_token.split("=")).map(item => item.trim()); + // config.bgi_tools.token.name = list[0] + // config.bgi_tools.token.value = list[1] + + const separatorIndex = bgi_tools_token.indexOf("="); + if (separatorIndex !== -1) { + config.bgi_tools.token.name = bgi_tools_token.substring(0, separatorIndex).trim(); + config.bgi_tools.token.value = bgi_tools_token.substring(separatorIndex + 1).trim(); + } else { + config.bgi_tools.token.name = bgi_tools_token.trim(); + config.bgi_tools.token.value = ""; + } + + + config.bgi_tools.api.httpPullJsonConfig = settings.bgi_tools_http_pull_json_config + config.bgi_tools.api.httpPushAllJsonConfig = settings.bgi_tools_http_push_all_json_config + config.bgi_tools.api.httpPushAllCountryConfig = settings.bgi_tools_http_push_all_country_config + config.bgi_tools.open.open_push = settings.bgi_tools_open_push + log.debug(`|bgi_tools:{1}`, JSON.stringify(config.bgi_tools)) + // const text = file.readTextSync(config.path.domain); + // log.info("config.path.domain:{1}",config.path.domain) + // log.info("text:{2}",text) + // const list = JSON.parse(text); + // log.info("list:{3}",[...list]) + const domainList = JSON.parse(file.readTextSync(config.path.domain)) || [{ + name: undefined, + type: undefined, + hasOrder: false, + list: [] + }] + + config.domainList.push(...domainList) + + config.domainList.forEach(item => { + if (!config.domainNames.has(item.name)) { + config.domainNames.add(item.name) + } + config.domainMap.set(item.name, item.list); + if (item?.hasOrder) { + let index = 1 + //设置顺序 + item.list.forEach(item2 => { + if (!config.itemNames.has(item2)) { + config.itemNames.add(item2) + } + config.domainOrderMap.set(item2, index) + config.domainItemsMap.set(item2, item.name) + index++ + }) + } + + }) + config.run.config = settings.run_config || config.run.config + if (config.domainList.length <= 0) { + throw new Error("配置文件缺失或读取异常!") + } + let loadList = await getValueByMultiCheckboxName('auto_load') || [] + const loads = loadList.map(item => { + const load = LoadMap.get(item); + if (!load) { + throw new Error(`无效加载方式: ${item}`); + } + let order = 1 + switch (load) { + case LoadType.input: + order = 1; + break; + case LoadType.uid: + order = 2; + break; + case LoadType.bgi_tools: + order = 3; + break; + } + return {load: load, order: order} + }) + loads.sort((a, b) => a.order - b.order) + config.run.loads = loads +} + + +export { + config, LoadType, LoadMap, + checkKey, + initSettings, + getMultiCheckboxMap, + getValueByMultiCheckboxName, + initConfig +} \ No newline at end of file diff --git a/repo/js/AutoPlan/config/countryList.json b/repo/js/AutoPlan/config/countryList.json new file mode 100644 index 000000000..a4ec3e88e --- /dev/null +++ b/repo/js/AutoPlan/config/countryList.json @@ -0,0 +1,9 @@ +[ +"蒙德", +"璃月", +"稻妻", +"须弥", +"枫丹", +"纳塔", +"挪德卡莱" +] \ No newline at end of file diff --git a/repo/js/AutoPlan/config/domain.json b/repo/js/AutoPlan/config/domain.json new file mode 100644 index 000000000..cb089d713 --- /dev/null +++ b/repo/js/AutoPlan/config/domain.json @@ -0,0 +1,324 @@ +[ + { + "name": "无光的深都", + "type": "天赋", + "hasOrder": true, + "list": [ + "「月光」的哲学", + "「乐园」的哲学", + "「浪迹」的哲学" + ] + }, + { + "name": "蕴火的幽墟", + "type": "天赋", + "hasOrder": true, + "list": [ + "「角逐」的哲学", + "「焚燔」的哲学", + "「纷争」的哲学" + ] + }, + { + "name": "苍白的遗荣", + "type": "天赋", + "hasOrder": true, + "list": [ + "「公平」的哲学", + "「正义」的哲学", + "「秩序」的哲学" + ] + }, + { + "name": "昏识塔", + "type": "天赋", + "hasOrder": true, + "list": [ + "「诤言」的哲学", + "「巧思」的哲学", + "「笃行」的哲学" + ] + }, + { + "name": "董色之庭", + "type": "天赋", + "hasOrder": true, + "list": [ + "「浮世」的哲学", + "「风雅」的哲学", + "「天光」的哲学" + ] + }, + { + "name": "太山府", + "type": "天赋", + "hasOrder": true, + "list": [ + "「繁荣」的哲学", + "「勤劳」的哲学", + "「黄金」的哲学" + ] + }, + { + "name": "忘却之峡", + "type": "天赋", + "hasOrder": true, + "list": [ + "「自由」的哲学", + "「抗争」的哲学", + "「诗文」的哲学" + ] + } + , + { + "name": "失落的月庭", + "type": "武器", + "hasOrder": true, + "list": [ + "奇巧秘器的真愿", + "长夜燧火的烈辉", + "终北遗嗣的煌熠" + ] + }, + { + "name": "深古瞭望所", + "type": "武器", + "hasOrder": true, + "list": [ + "贡祭炽心的荣膺", + "谚妄圣主的神面", + "神合秘烟的启示" + ] + }, + { + "name": "深潮的余响", + "type": "武器", + "hasOrder": true, + "list": [ + "悠古弦音的回响", + "纯圣露滴的真粹", + "无垢之海的金杯" + ] + }, + { + "name": "有顶塔", + "type": "武器", + "hasOrder": true, + "list": [ + "谧林涓露的金符", + "绿洲花园的真谛", + "烈日威权的旧日" + ] + }, + { + "name": "砂流之庭", + "type": "武器", + "hasOrder": true, + "list": [ + "远海夷地的金枝", + "鸣神御灵的勇武", + "今昔剧画之鬼人" + ] + }, + { + "name": "震雷连山密宫", + "type": "武器", + "hasOrder": true, + "list": [ + "孤云寒林的神体", + "雾海云间的转还", + "漆黑陨铁的一块" + ] + }, + { + "name": "塞西莉亚苗圃", + "type": "武器", + "hasOrder": true, + "list": [ + "高塔孤王的碎梦", + "凛风奔狼的怀乡", + "狮牙斗士的理想" + ] + } + , + { + "name": "月童的库藏", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "风起之日", + "晨星与月的晓歌" + ] + }, + { + "name": "霜凝的机枢", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "纺月的夜歌", + "穹境示现之夜" + ] + }, + { + "name": "荒废砌造坞", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "深廊终曲", + "长夜之誓" + ] + }, + { + "name": "虹灵的净土", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "黑曜秘典", + "城勇者绘卷" + ] + }, + { + "name": "褪色的剧场", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "未竟的遐思", + "谐律异想断章" + ] + }, + { + "name": "临瀑之城", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "回声之林夜话", + "昔时之歌" + ] + }, + { + "name": "罪祸的终末", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "黄金剧团", + "逐影猎人" + ] + }, + { + "name": "熔铁的孤塞", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "花海甘露之光", + "水仙之梦" + ] + }, + { + "name": "赤金的城墟", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "乐园遗落之花", + "沙上楼阁史话" + ] + }, + { + "name": "缘觉塔", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "饰金之梦", + "深林的记忆" + ] + }, + { + "name": "沉眠之庭", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "海染砗磲", + "华馆梦醒形骸记" + ] + }, + { + "name": "花染之庭", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "绝缘之旗印", + "追忆之注连" + ] + }, + { + "name": "岩中幽谷", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "辰砂往生录", + "来歆余响" + ] + }, + { + "name": "华池岩柚", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "染血的骑士道", + "昔日宗室之仪" + ] + }, + { + "name": "无妄引答密宫", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "炽烈的炎之魔女", + "渡过烈火的贤人" + ] + }, + { + "name": "孤云凌霄之处", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "悠古的磐岩", + "逆飞的流星" + ] + }, + { + "name": "山脊守望", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "千岩牢固", + "苍白之火" + ] + }, + { + "name": "芬德尼尔之顶", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "冰风迷途的勇士", + "沉沦之心" + ] + }, + { + "name": "铭记之谷", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "翠绿之影", + "被怜爱的少女" + ] + }, + { + "name": "仲夏庭园", + "type": "圣遗物", + "hasOrder": false, + "list": [ + "如雷的盛怒", + "平息鸣雷的尊者" + ] + } +] \ No newline at end of file diff --git a/repo/js/AutoPlan/main.js b/repo/js/AutoPlan/main.js new file mode 100644 index 000000000..5d3689cd8 --- /dev/null +++ b/repo/js/AutoPlan/main.js @@ -0,0 +1,544 @@ +import {config, initConfig, initSettings, LoadType} from './config/config'; +import {ocrUid} from './utils/uid'; +import {getDayOfWeek, outDomainUI, throwError} from './utils/tool'; +import {pullJsonConfig, pushAllCountryConfig, pushAllJsonConfig} from './utils/bgi_tools'; +import {ocrPhysical} from "./utils/physical"; + +/** + * 自动执行秘境任务的异步函数 + * @param {Object} autoFight - 包含秘境自动配置参数的对象 + * @returns {Promise} - 执行完成后返回的Promise + */ +async function autoDomain(autoFight) { + log.info(`{0}`, "开始执行秘境任务") + log.warn(`{0}`, "非体力耗尽情况下(受本体限制),等待退出秘境时间较长") + //定死做预留冗余 先不实现 不能指定次数 只能指定启用 + let physical_domain = autoFight?.physical + // || [ + // {order: 0, name: "原粹树脂", count: 1, open: true}, + // {order: 1, name: "浓缩树脂", count: 0, open: false}, + // {order: 2, name: "须臾树脂", count: 0, open: false}, + // {order: 3, name: "脆弱树脂", count: 0, open: false}, + // ] + + if ((!physical_domain) || physical_domain.filter(item => item?.open).length === 0) { + const names = config.user.physical.names; + physical_domain = [] + names.forEach((name, index) => { + physical_domain.push({order: index, name: name, open: index === 0}) + }) + } + + physical_domain.sort((a, b) => a.order - b.order) + // 不包含原粹树脂的和 + const noOriginalSum = physical_domain.filter(item => item?.name.trim() !== "原粹树脂") + .filter(item => item?.open).length;//求和 + // 只包含原粹树脂的和 + const originalSum = physical_domain.filter(item => item?.name?.trim() === "原粹树脂") + .filter(item => item?.open).length; + const resinPriorityList = physical_domain.filter(item => item?.open).map(item => item?.name?.trim()) + // /** 树脂使用优先级列表 */ + // resinPriorityList: string[]; + // /** 使用原粹树脂次数 */ + // originalResinUseCount: number; + // /** 使用浓缩树脂次数 */ + // condensedResinUseCount: number; + // /** 使用须臾树脂次数 */ + // transientResinUseCount: number; + // /** 使用脆弱树脂次数 */ + // fragileResinUseCount: number; + await sleep(1000) + //流程->返回主页 打开地图 返回主页 + const physicalOcr = await ocrPhysical(true, true) + config.user.physical.current = physicalOcr.current + config.user.physical.min = physicalOcr.min + const physical = config.user.physical + if (physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) { + throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`) + } + // 创建秘境参数对象,初始化值为0 + let domainParam = new AutoDomainParam(); + //关闭分解 + domainParam.autoArtifactSalvage = false + //关闭榨干原粹树脂 + domainParam.specifyResinUse = true + //配置树脂使用优先级 + if (resinPriorityList.length > 0) { + domainParam.SetResinPriorityList(...resinPriorityList) + } + // log.debug(`开始执行秘境任务`) + //秘境名称 + domainParam.DomainName = autoFight.domainName || domainParam.DomainName; + log.debug(`秘境名称:${domainParam.DomainName}`) + + //队伍名称 + domainParam.PartyName = autoFight.partyName || domainParam.PartyName; + log.debug(`队伍名称:${domainParam.PartyName}`) + + if (autoFight.sundaySelectedValue) { + //周日|限时选择的值 + domainParam.SundaySelectedValue = "" + (autoFight.sundaySelectedValue || domainParam.SundaySelectedValue); + } + log.debug(`周日|限时选择的值:${domainParam.SundaySelectedValue}`) + //副本轮数 + try { + domainParam.DomainRoundNum = parseInt((autoFight.domainRoundNum || domainParam.DomainRoundNum) + ""); + } catch (e) { + log.debug(`副本轮数:${autoFight.domainRoundNum}`) + throwError(e.message) + } + log.debug(`副本轮数:${domainParam.DomainRoundNum}`) + try { + // 复活重试 + for (let i = 0; i < config.run.retry_count; i++) { + try { + await dispatcher.RunAutoDomainTask(domainParam); + // 其他场景不重试 + break; + } catch (e) { + const errorMessage = e.message + // 只有选择了秘境的时候才会重试 + if (errorMessage.includes("复活") && domainParam.DomainName) { + continue; + } + throw e; + } + } + } finally { + log.info(`{0}`, "执行完成") + // 退出秘境 + await outDomainUI() + } +} + +/** + * 自动执行地脉花任务的异步函数 + * @param autoLeyLineOutcrop + * @returns {Promise} + */ +async function autoLeyLineOutcrop(autoLeyLineOutcrop) { + //todo :地脉花 + // autoLeyLineOutcrop = { + // "count": 0, + // "country": "country_cb3d792be8db", + // "leyLineOutcropType": "leyLineOutcropType_f259b77fabcb", + // // "isResinExhaustionMode": true, + // // "openModeCountMin": true, + // "useAdventurerHandbook": false, + // "friendshipTeam": "friendshipTeam_7122cab56b16", + // "team": "team_d0798ca3aa27", + // "timeout": 0, + // "isGoToSynthesizer": false, + // "useFragileResin": false, + // "useTransientResin": false, + // "isNotification": false + // } + + + log.info(`{0}`, "开始执行地脉任务") + // if (true) { + // log.warn("地脉 暂不支持") + // return + // } + let param = new AutoLeyLineOutcropParam(parseInteger(autoLeyLineOutcrop.count + ""), autoLeyLineOutcrop.country, autoLeyLineOutcrop.leyLineOutcropType); + // let param = new AutoLeyLineOutcropParam(); + // param.count = parseInteger(autoLeyLineOutcrop.count+""); + // param.country = autoLeyLineOutcrop.country; + // param.leyLineOutcropType = autoLeyLineOutcrop.leyLineOutcropType; + //和本体保持一致 + param.useAdventurerHandbook = !autoLeyLineOutcrop.useAdventurerHandbook; + param.friendshipTeam = autoLeyLineOutcrop.friendshipTeam; + param.team = autoLeyLineOutcrop.team; + param.timeout = autoLeyLineOutcrop.timeout; + param.isGoToSynthesizer = autoLeyLineOutcrop.isGoToSynthesizer; + param.useFragileResin = autoLeyLineOutcrop.useFragileResin; + param.useTransientResin = autoLeyLineOutcrop.useTransientResin; + param.isNotification = autoLeyLineOutcrop.isNotification; + + param.isResinExhaustionMode = true; + param.openModeCountMin = true; + await sleep(1000) + // 复活重试 + for (let i = 0; i < config.run.retry_count; i++) { + try { + await dispatcher.RunAutoLeyLineOutcropTask(param); + // 其他场景不重试 + break; + } catch (e) { + const errorMessage = e.message + // 只有选择了秘境的时候才会重试 + if (errorMessage.includes("复活")) { + continue; + } + throw e; + } + } +} + +/** + * 自动执行列表处理函数 + * @param {Array} autoRunOrderList - 包含自动配置的数组 + */ +async function autoRunList(autoRunOrderList) { + //计划执行 + for (const item of autoRunOrderList) { + await sleep(3000) + if (item.runType === config.user.runTypes[0]) { + await autoDomain(item.autoFight); + } else if (item.runType === config.user.runTypes[1]) { + await autoLeyLineOutcrop(item.autoLeyLineOutcrop); + } + } +} + +// 辅助函数:安全地解析 day 字段 +function parseInteger(day) { + if (day == null || String(day).trim() === "") { + return undefined; // 空值或无效值返回 undefined + } + const parsedDay = parseInt(String(day).trim(), 10); + return isNaN(parsedDay) ? undefined : parsedDay; // 非法数字返回 undefined +} + +/** + * 根据不同的加载方式加载秘境配置 + * @param {string} Load - 加载方式类型,如uid或input + * @param {Set} autoOrderSet - 用于存储秘境顺序的Set集合 + * @param {string} runConfig - 输入的配置字符串,仅在Load为input时使用 + */ +async function loadMode(Load, autoOrderSet, runConfig) { + switch (Load) { + case LoadType.input: + // 通过输入字符串方式加载配置 + if (runConfig) { + // 处理输入字符串:去除首尾空格,将中文逗号替换为英文逗号,然后按逗号分割 + runConfig.trim().replaceAll(',', ',').split(",").forEach( + item => { + // 将当前项按"|"分割成数组 + let arr = item.split("|") + // 类型|执行日期|执行顺序 + let index = 0 + let runType = arr[index]; // 解析运行类型 + index++ + const rawDays = arr[index]; + let days = (rawDays != null && String(rawDays).trim() !== "") + ? String(rawDays).split('/').map(d => parseInt(d.trim(), 10)).filter(d => !isNaN(d)) + : []; + // let days = arr[index].trim() !== "" + // ? arr[index].split('/').map(d => parseInt(d.trim())).filter(d => !isNaN(d)) + // : []; + index++ + // 解析顺序值,处理可能的无效值 + let order = (() => { + const rawOrder = arr[index]; // 获取原始值 + if (rawOrder == null || String(rawOrder).trim() === "") { + return 0; // 若为空或无效值,默认返回 0 + } + const parsedOrder = parseInt(String(rawOrder).trim(), 10); // 转换为整数 + return isNaN(parsedOrder) ? 0 : parsedOrder; // 若转换失败,返回默认值 0 + })(); + index++ + + // 创建秘境顺序对象 + let autoOrder = { + order: order, // 顺序值 + // day: day,// 执行日期 + runType: runType, // 运行类型 + days: days, // 执行日期(数组) + autoFight: undefined, // 秘境信息对象 + autoLeyLineOutcrop: undefined // 地脉信息对象 + } + + + if (!config.user.runTypes.includes(runType)) { + throwError(`运行类型${runType}输入错误`) + } else if (config.user.runTypes[0] === runType) { + // 创建秘境信息对象 + let autoFight = { + domainName: undefined,//秘境名称 + partyName: undefined,//队伍名称 + sundaySelectedValue: 1,//周日|限时选择的值 + domainRoundNum: 0,//副本轮数 + } + + //"|队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日,..." + let partyName = arr[index]; // 解析队伍名称 + index++ + let domainName = arr[index]; // 解析秘境名称 + index++ + let domainRoundNum = arr[index]; // 解析副本轮数 + index++ + let sundaySelectedValue = "1" + if (index <= arr.length - 1) + sundaySelectedValue = arr[index]; // 解析周日|限时选择的值 + + // 检查秘境名称是否有效 + if (!config.domainNames.has(domainName)) { + //秘境名称没有记录 查询是否是物品名称 + if (config.itemNames.has(domainName)) { + const domainNameTemp = config.domainItemsMap.get(domainName); + if (!domainNameTemp) { + throw new Error(`${domainName} 输入错误`); + } + if (index <= arr.length - 1) { + const domainSelectedValue = parseInt(config.domainOrderMap.get(domainName) + ""); + sundaySelectedValue = domainSelectedValue + } + domainName = domainNameTemp + } else { + throw new Error(`${domainName} 输入错误`); + } + } + + // 设置秘境信息的各个属性 + autoFight.partyName = partyName // 队伍名称 + autoFight.domainName = domainName // 秘境名称 + autoFight.domainRoundNum = domainRoundNum // 副本轮数 + autoFight.sundaySelectedValue = sundaySelectedValue // 周日|限时选择的值 + + autoOrder.autoFight = autoFight // 将秘境信息对象添加到秘境顺序对象中 + } else if (config.user.runTypes[1] === runType) { + //"|队伍名称|国家|刷几轮|花类型|好感队|是否使用脆弱树脂|是否使用须臾树脂|是否前往合成台合成浓缩树脂|是否使用冒险之证|发送详细通知|战斗超时时间,..." + let autoLeyLineOutcrop = { + count: 0, // 刷几次(0=自动/无限) + country: undefined, // 国家地区 + leyLineOutcropType: undefined, // 需映射为经验/摩拉 + useAdventurerHandbook: false, // 是否使用冒险之证 + friendshipTeam: "", // 好感队伍ID + team: "", // 主队伍ID + timeout: 120, // 超时时间(秒) + isGoToSynthesizer: false, // 是否前往合成台 + useFragileResin: false, // 使用脆弱树脂 + useTransientResin: false, // 使用须臾树脂(须臾=Transient) + isNotification: false // 是否通知 + } + autoLeyLineOutcrop.team = arr[index] + index++ + autoLeyLineOutcrop.country = arr[index] + index++ + autoLeyLineOutcrop.count = arr[index] + index++ + autoLeyLineOutcrop.leyLineOutcropType = arr[index] + index++ + if (index <= arr.length - 1) + autoLeyLineOutcrop.friendshipTeam = arr[index] + index++ + + if (index <= arr.length - 1) + autoLeyLineOutcrop.useFragileResin = (arr[index] != null && arr[index].trim() !== "") + index++ + if (index <= arr.length - 1) + autoLeyLineOutcrop.useTransientResin = (arr[index] != null && arr[index].trim() !== "") + index++ + if (index <= arr.length - 1) + autoLeyLineOutcrop.isGoToSynthesizer = (arr[index] != null && arr[index].trim() !== "") + index++ + if (index <= arr.length - 1) + autoLeyLineOutcrop.useAdventurerHandbook = (arr[index] != null && arr[index].trim() !== "") + index++ + if (index <= arr.length - 1) + autoLeyLineOutcrop.isNotification = (arr[index] != null && arr[index].trim() !== "") + + index++ + if (index <= arr.length - 1) + autoLeyLineOutcrop.timeout = parseInteger(arr[index]) + + autoOrder.autoLeyLineOutcrop = autoLeyLineOutcrop // 将地脉信息对象添加到顺序对象中 + } + + // 将秘境顺序对象添加到列表中 + autoOrderSet.add(autoOrder) + } + ) + } + break + + case LoadType.uid: + + // 通过UID方式加载配置 + const uid = config.user.uid || (await ocrUid()) // 获取用户UID,如果未配置则通过OCR识别获取 + // const configAutoFightOrderMap = JSON.parse(file.readTextSync(config.path.runConfig)) || new Map() // 读取本地配置文件并转换为Map对象 + // const uidConfigList = configAutoFightOrderMap.get(uid + "") || []; // 获取当前UID对应的配置列表 + + const configAutoFightOrderMap = JSON.parse(file.readTextSync(config.path.runConfig)) || {} // 读取本地配置文件 + const uidConfigList = configAutoFightOrderMap[uid + ""] || []; // 获取当前UID对应的配置列表 + if (uidConfigList?.length > 0) { + // 如果配置列表不为空,遍历并添加到结果集合中 + uidConfigList.forEach(item => { + // 将秘境顺序对象添加到列表中 + // 主逻辑优化 + // if (item.day !== undefined) { + // item.day = parseInteger(item.day); + // } + if (item.days && item.days.length > 0) { + item.days = item.days.map(day => parseInteger(day)) + // item.day = parseInteger(item.day); + } + autoOrderSet.add(item) + }) + } + break + case LoadType.bgi_tools: + // 通过bgi_tools方式加载配置 + log.info(`开始拉取bgi_tools配置`) + const uidConfigListBgiTools = await pullJsonConfig(config.user.uid + '', config.bgi_tools.api.httpPullJsonConfig) || [] + if (uidConfigListBgiTools?.length > 0) { + // 如果配置列表不为空,遍历并添加到结果集合中 + uidConfigListBgiTools.forEach(item => { + // 将秘境顺序对象添加到列表中 + // 主逻辑优化 + if (item.days && item.days.length > 0) { + item.days = item.days.map(day => parseInteger(day)) + // item.day = parseInteger(item.day); + } + autoOrderSet.add(item) + }) + } + break + default: + throw new Error("请先配置加载方式"); + // break; + } +} + +/** + * 初始化执行顺序列表 + * @param {string} domainConfig - 输入的字符串,包含秘境顺序信息 + * @returns {Array} 返回处理后的秘境顺序列表 + */ +async function initRunOrderList(domainConfig) { + const autoFightOrderSet = new Set() // 存储秘境顺序列表的数组 + /* let te = { + order: 1, // 顺序值 + day: 0,// 执行日期 + autoFight: { + domainName: undefined,//秘境名称 + partyName: undefined,//队伍名称 + sundaySelectedValue: undefined,//周日|限时选择的值 + domainRoundNum: undefined,//副本轮数 + } // 秘境信息对象 + }*/ + // let Load = LoadType.uid + + for (const Load of config.run.loads) { + await loadMode(Load.load, autoFightOrderSet, domainConfig); + } + + // 检查是否已配置秘境 + if (!autoFightOrderSet || autoFightOrderSet.size <= 0) { + throw new Error("请先配置体力配置"); + } + // 返回处理后的秘境顺序列表 + let from = Array.from(autoFightOrderSet); + let dayOfWeek = await getDayOfWeek(); + log.debug(`old-from:{0}`, JSON.stringify(from)) + from = from + //过滤掉不执行的秘境 + .filter(item => config.user.runTypes.includes(item.runType)) + .filter(item => { + // if (item.day) { + // return item.day === dayOfWeek.day + // } + log.debug(`[{1}]item.days.length:{0}`, dayOfWeek.day, item?.days?.length || 0) + if (item.days && item.days.length > 0) { + const includes = item.days.includes(dayOfWeek.day); + log.debug(`[{1}]item.days:{0}`, dayOfWeek.day, JSON.stringify(item.days)) + return includes; + } + return true + }) + from.sort((a, b) => b.order - a.order) + log.debug(`from:{0}`, JSON.stringify(from)) + return from; +} + +/** + * 初始化函数 + * 该函数用于执行初始化操作,使用async/await处理异步操作 + */ +async function init() { + // 调用initConfig函数并等待其完成 + // 这是一个异步初始化配置的步骤 + await initSettings() + await initConfig(); +} + +/** + * 主函数,用于执行秘境自动刷取任务 + * @async + */ +async function main() { + // 初始化配置 + await init(); + if (config.bgi_tools.open.open_push) { + log.info(`开始推送bgi_tools配置`) + await pushAllJsonConfig(JSON.parse(file.readTextSync(config.path.domain)), config.bgi_tools.api.httpPushAllJsonConfig, config.bgi_tools.token) + await pushAllCountryConfig(JSON.parse(file.readTextSync(config.path.countryList)), config.bgi_tools.api.httpPushAllCountryConfig, config.bgi_tools.token) + } + // 获取配置 + let runConfig = config.run.config; + //"队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日|周几执行(0-6)不填默认执行|执行顺序,..." + const autoRunOrderList = await initRunOrderList(runConfig); + const list = autoRunOrderList.filter(item => + (item.runType === config.user.runTypes[0] && item?.autoFight.domainRoundNum > 0) + || (item.runType === config.user.runTypes[1] && item?.autoLeyLineOutcrop.count > 0) + ) + if (list?.length > 0) { + //循环跑 + while (true) { + await autoRunList(list); + if (config.run.loop_plan) { + // 重新获取当前体力值 + const physicalOcr = await ocrPhysical(true, true); + config.user.physical.current = physicalOcr.current; + //循环 + if (config.user.physical.current < config.user.physical.min) { + //体力耗尽 + break + } + } else { + //不循环 + break + } + + } + } else { + log.info(`本日无计划`) + } + +} + +(async function () { + // await test() + // await test1() + // await test2() + await main() +})() + +async function test() { + await init(); + const text = file.readTextSync(config.path.domain); + // log.info("settings:{1}",config.info.settings) + // log.info("text:{1}",text) + const list = JSON.parse(text); + // log.info("list:{1}",list) + log.info("httpPullJsonConfig:{1}", config.bgi_tools.api.httpPushAllJsonConfig) + log.info("|test==>config.bgi_tools:{1}", JSON.stringify(config.bgi_tools)) + await pushAllJsonConfig(list, config.bgi_tools.api.httpPushAllJsonConfig) +} + + +async function test1() { + await init(); + // log.info("text:{1}",text) + // log.info("list:{1}",list) + log.info("httpPullJsonConfig:{1}", config.bgi_tools.api.httpPullJsonConfig) + log.info("|test==>config.bgi_tools:{1}", JSON.stringify(config.bgi_tools)) + const list = await pullJsonConfig(config.user.uid, config.bgi_tools.api.httpPullJsonConfig) + log.info("list:{1}", JSON.stringify(list)) +} + +async function test2() { + await init(); + await outDomainUI(); +} \ No newline at end of file diff --git a/repo/js/AutoPlan/manifest.json b/repo/js/AutoPlan/manifest.json new file mode 100644 index 000000000..16a5860ba --- /dev/null +++ b/repo/js/AutoPlan/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "自动体力计划", + "version": "0.0.1", + "description": "", + "settings_ui": "settings.json", + "main": "main.js", + "bgi_version": "0.57.2", + "key": "oiJbmjU2R0NniiwiZxh", + "authors": [ + { + "name": "云端客", + "links": "https://github.com/Kirito520Asuna" + } + ], + "dependencies": [], + "http_allowed_urls": [ + "https://*", + "http://*" + ] +} \ No newline at end of file diff --git a/repo/js/AutoPlan/md/bgi-tools-config-ui-01.jpg b/repo/js/AutoPlan/md/bgi-tools-config-ui-01.jpg new file mode 100644 index 000000000..4beb1218b Binary files /dev/null and b/repo/js/AutoPlan/md/bgi-tools-config-ui-01.jpg differ diff --git a/repo/js/AutoPlan/md/bgi-tools-config-ui-02.jpg b/repo/js/AutoPlan/md/bgi-tools-config-ui-02.jpg new file mode 100644 index 000000000..d261f5585 Binary files /dev/null and b/repo/js/AutoPlan/md/bgi-tools-config-ui-02.jpg differ diff --git a/repo/js/AutoPlan/md/bgi-tools-config-ui-init.jpg b/repo/js/AutoPlan/md/bgi-tools-config-ui-init.jpg new file mode 100644 index 000000000..deff1c215 Binary files /dev/null and b/repo/js/AutoPlan/md/bgi-tools-config-ui-init.jpg differ diff --git a/repo/js/AutoPlan/md/bgi-tools-config-ui.jpg b/repo/js/AutoPlan/md/bgi-tools-config-ui.jpg new file mode 100644 index 000000000..4e25c1c6d Binary files /dev/null and b/repo/js/AutoPlan/md/bgi-tools-config-ui.jpg differ diff --git a/repo/js/AutoPlan/md/bgi-tools-login.jpg b/repo/js/AutoPlan/md/bgi-tools-login.jpg new file mode 100644 index 000000000..85d74d394 Binary files /dev/null and b/repo/js/AutoPlan/md/bgi-tools-login.jpg differ diff --git a/repo/js/AutoPlan/settings.json b/repo/js/AutoPlan/settings.json new file mode 100644 index 000000000..754d7da14 --- /dev/null +++ b/repo/js/AutoPlan/settings.json @@ -0,0 +1,71 @@ +[ + { + "name": "key", + "type": "input-text", + "label": "密钥(去看文档)" + }, + { + "name": "auto_load", + "type": "multi-checkbox", + "label": "加载模式\n说明太长 去看文档", + "options": ["输入加载","UID加载","bgi_tools加载"] , + "default": ["输入加载","UID加载"] + }, + { + "name": "run_config", + "type": "input-text", + "label": "自动秘境计划配置\n语法:说明太长 去看文档", + "default": "" + }, + { + "name": "loop_plan", + "type": "checkbox", + "label": "循环体力计划\n体力耗尽自动终止" + }, + { + "name": "retry_count", + "type": "select", + "label": "复活重试次数", + "options": [ + "1","2","3", + "4","5","6", + "7","8","9" + ] , + "default": "3" + }, + { + "type": "separator" + }, + { + "type": "separator" + }, + { + "name": "bgi_tools_http_pull_json_config", + "type": "input-text", + "label": "bgi_tools拉取配置api(去看文档)", + "default": "http://127.0.0.1:8081/bgi/auto/plan/json" + }, + { + "name": "bgi_tools_open_push", + "type": "checkbox", + "label": "开始推送bgi_tools配置" + }, + { + "name": "bgi_tools_http_push_all_json_config", + "type": "input-text", + "label": "bgi_tools推送全部配置api(去看文档)", + "default": "http://127.0.0.1:8081/bgi/auto/plan/domain/json/all" + }, + { + "name": "bgi_tools_http_push_all_country_config", + "type": "input-text", + "label": "bgi_tools推送全部国家配置api(去看文档)", + "default": "http://127.0.0.1:8081/bgi/auto/plan/country/json/all" + }, + { + "name": "bgi_tools_token", + "type": "input-text", + "label": "bgi_tools授权token 语法:tokenName=tokenValue(去看文档)", + "default": "Authorization= " + } +] \ No newline at end of file diff --git a/repo/js/AutoPlan/utils/bgi_tools.js b/repo/js/AutoPlan/utils/bgi_tools.js new file mode 100644 index 000000000..45dd88889 --- /dev/null +++ b/repo/js/AutoPlan/utils/bgi_tools.js @@ -0,0 +1,80 @@ + +/** + * 拉取对应uid的Json数据 + * @param uid + * @param http_api + * @returns {Promise} + */ +async function pullJsonConfig(uid, http_api) { + http_api += "?uid=" + uid + const res = await http.request("GET", http_api + // , JSON.stringify({"Content-Type": "application/json"}) + ) + 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 +} + +/** + * 推送全部Json数据 + * @param Json + * @param http_api + * @returns {Promise} + */ +async function pushAllJsonConfig(list = [], http_api,token={name: "Authorization", value: ''}) { + log.info(`list:{1},http:{2}`, list, http_api) + let value = { + "Content-Type": "application/json", + [token.name]: token.value + }; + + const res = await http.request("POST", http_api, JSON.stringify({json: JSON.stringify(list)}), JSON.stringify(value)) + + 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 +} + +/** + * 推送全部国家Json数据 + * @param list + * @param http_api + * @param token + * @returns {Promise} + */ +async function pushAllCountryConfig(list = [], http_api,token={name: "Authorization", value: ''}) { + log.info(`list:{1},http:{2}`, list, http_api) + let value = { + "Content-Type": "application/json", + [token.name]: token.value + }; + + const res = await http.request("POST", http_api, JSON.stringify({json: JSON.stringify(list)}), JSON.stringify(value)) + + 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 +} +export { + pullJsonConfig, + pushAllJsonConfig, + pushAllCountryConfig +} \ No newline at end of file diff --git a/repo/js/AutoPlan/utils/physical.js b/repo/js/AutoPlan/utils/physical.js new file mode 100644 index 000000000..a399925d4 --- /dev/null +++ b/repo/js/AutoPlan/utils/physical.js @@ -0,0 +1,165 @@ +import {getJsonPath, toMainUi,throwError} from "./tool"; +//==================================================== +const genshinJson = { + width: 1920,//genshin.width, + height: 1080,//genshin.height, +} +// const MinPhysical = settings.minPhysical?parseInt(settings.minPhysical+''):parseInt(20+'') +// const OpenModeCountMin = settings.openModeCountMin +// let AlreadyRunsCount=0 +// let NeedRunsCount=0 +const TemplateOrcJson={x: 1568, y: 16, width: 225, height: 60,} +//==================================================== + + +/** + * 从字符串中提取数字并组合成一个整数 + * @param {string} str - 包含数字的字符串 + * @returns {number} - 由字符串中所有数字组合而成的整数 + */ +async function saveOnlyNumber(str,defaultValue=0) { + // 使用正则表达式匹配字符串中的所有数字 + // \d+ 匹配一个或多个数字 + // .join('') 将匹配到的数字数组连接成一个字符串 + // parseInt 将连接后的字符串转换为整数 + try { + return parseInt(str.match(/\d+/g).join('')); + }catch (e) { + return defaultValue + } +} + +/** + * 识别原粹树脂(体力)的函数 + * @param {boolean} [opToMainUi=false] - 是否操作到主界面 + * @param {boolean} [openMap=false] - 是否打开地图界面 + * @param {number} [minPhysical=20] - 最小可执行体力值 + * @param {boolean} [isResinExhaustionMode=true] - 是否启用体力识别功能 + * @returns {Promise} 返回一个包含识别结果的Promise对象 + * - ok {boolean}: 是否可执行(体力是否足够) + * - min {number}: 最小可执行体力值 + * - current {number}: 当前剩余体力值 + */ +async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isResinExhaustionMode=true) { + // 检查是否启用体力识别功能,如果未启用则直接返回默认结果 + if (!isResinExhaustionMode) { + log.info(`===未启用===`) + return { + ok: true, + min: 0, + current: 0, + } + } + log.debug(`===开始识别原粹树脂===`) + let ms = 1000 // 定义操作延迟时间(毫秒) + if (opToMainUi) { + await sleep(ms) + await toMainUi(); // 切换到主界面 + } + + if (openMap){ + await sleep(ms) + //打开地图界面 + await keyPress('M') + } + await sleep(ms) + log.debug(`===[点击+]===`) + //点击+ 按钮 x=1264,y=39,width=18,height=19 + let add_buttonJSON = getJsonPath('add_button'); + let add_objJson = { + path: `${add_buttonJSON.path}${add_buttonJSON.name}${add_buttonJSON.type}`, + x: 1373, + y: 22, + width: 52, + height: 49, + } + let templateMatchAddButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${add_objJson.path}`), add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height); + let regionA = captureGameRegion() + // let deriveCrop = regionA.DeriveCrop(add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height); + try { + let buttonA = regionA.find(templateMatchAddButtonRo); + + await sleep(ms) + if (!buttonA.isExist()) { + log.error(`${add_objJson.path}匹配异常`) + throwError(`${add_objJson.path}匹配异常`) + } + await buttonA.click() + }finally { + // deriveCrop.dispose() + regionA.dispose() + } + + await sleep(ms) + + log.debug(`===[定位原粹树脂]===`) + //定位月亮 + let jsonPath = getJsonPath('yue'); + let tmJson = { + path: `${jsonPath.path}${jsonPath.name}${jsonPath.type}`, + x: TemplateOrcJson.x, + y: TemplateOrcJson.y, + width: TemplateOrcJson.width, + height: TemplateOrcJson.height, + } + let templateMatchButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${tmJson.path}`), tmJson.x, tmJson.y, tmJson.width, tmJson.height); + let region =captureGameRegion() + let button + try { + button = region.find(templateMatchButtonRo); + await sleep(ms) + if ((!button)||!button.isExist()) { + log.error(`${tmJson.path} 匹配异常`) + throwError(`${tmJson.path} 匹配异常`) + } + }finally { + region.dispose() + } + + + log.debug(`===[识别原粹树脂]===`) + //识别体力 x=1625,y=31,width=79,height=30 / x=1689,y=35,width=15,height=26 + let ocr_obj = { + // x: 1623, + x: button.x + button.width, + // y: 32, + y: button.y, + // width: 61, + width: Math.abs(genshinJson.width - button.x - button.width), + height: 26 + } + + log.debug(`ocr_obj: x={x},y={y},width={width},height={height}`, ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height) + let region3 = captureGameRegion() + + try { + let recognitionObjectOcr = RecognitionObject.Ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height); + let res = region3.find(recognitionObjectOcr); + + log.debug(`[OCR原粹树脂]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`); + let text=res.text.split('/')[0] + let current = await saveOnlyNumber(text) + let execute = (current - minPhysical) >= 0 + log.debug(`最小可执行原粹树脂:{min},原粹树脂:{key}`, minPhysical, current,) + + // await keyPress('VK_ESCAPE') + return { + ok: execute, + min: minPhysical, + current: current, + } + } catch (e) { + throwError(`识别失败,err:${e.message}`) + } finally { + region3.dispose() + //返回地图操作 + if (opToMainUi) { + await toMainUi(); // 切换到主界面 + } + } + +} + +export { + ocrPhysical, +} \ No newline at end of file diff --git a/repo/js/AutoPlan/utils/tool.js b/repo/js/AutoPlan/utils/tool.js new file mode 100644 index 000000000..ed88b2548 --- /dev/null +++ b/repo/js/AutoPlan/utils/tool.js @@ -0,0 +1,348 @@ +/** + * 对指定区域进行OCR文字识别 + * @param {number} x - 区域左上角x坐标,默认为0 + * @param {number} y - 区域左上角y坐标,默认为0 + * @param {number} w - 区域宽度,默认为1920 + * @param {number} h - 区域高度,默认为1080 + * @returns {Promise} 返回识别到的文本内容,如果识别失败则返回null + */ +async function ocrRegion(x = 0, + y = 0, + w = 1920, + h = 1080) { + // 创建OCR识别对象,使用指定的坐标和尺寸 + let recognitionObjectOcr = RecognitionObject.Ocr(x, y, w, h); + // 捕获游戏区域图像 + let region3 = captureGameRegion() + try { + // 在捕获的区域中查找OCR识别对象 + let res = region3.find(recognitionObjectOcr); + // 返回识别到的文本内容,如果不存在则返回undefined + return res?.text + } catch (e) { + // 捕获并记录错误信息 + log.error("识别异常:{1}", e.message) + return null + } finally { + // 确保释放区域资源 + region3.dispose() + } +} + +/** + * 获取当前日期的星期信息 + * @param {boolean} [calibrationGameRefreshTime=true] 是否进行游戏刷新时间校准 + * @returns {Object} 返回包含星期数字和星期名称的对象 + */ +async function getDayOfWeek(calibrationGameRefreshTime = true) { + // 获取当前日期对象 + let today = new Date();//4点刷新 所以要减去4小时 + if (calibrationGameRefreshTime) { + today.setHours(today.getHours() - 4); // 减去 4 小 + } + // 获取当前日期是星期几(0代表星期日,1代表星期一,以此类推) + const day = today.getDay(); + // 创建包含星期名称的数组 + const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; + let weekDay = `${weekDays[day]}`; + + log.debug(`今天是[{day}]`, day) + log.debug(`今天是[{weekDays}]`, weekDay) + // 返回包含星期数字和对应星期名称的对象 + return { + day: day, + dayOfWeek: weekDay + } +} + +const commonPath = 'assets/' +const commonMap = new Map([ + ['main_ui', { + path: `${commonPath}`, + name: 'paimon_menu', + type: '.png', + }], + ['yue', { + path: `${commonPath}`, + name: 'yue', + type: '.png', + }], + ['200', { + path: `${commonPath}`, + name: '200', + type: '.png', + }], + ['add_button', { + path: `${commonPath}`, + name: 'add_button', + type: '.jpg', + }], + ['out_domain', { + path: `${commonPath}`, + name: 'out_domain', + type: '.jpg', + }], +]) +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方法获取指定键对应的值 +} + +// 判断是否在主界面的函数 +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(); + try { + let res = captureRegion.find(paimonMenuRo); + return !res.isEmpty(); + } finally { + captureRegion.dispose() + } + +}; + +async function toMainUi() { + let ms = 300 + let index = 1 + await sleep(ms); + while (!isInMainUI()) { + await sleep(ms); + await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面 + await sleep(ms); + if (index > 3) { + throwError(`多次尝试返回主界面失败`); + } + index += 1 + } + +} + +const isInOutDomainUI = async () => { + // // let name = '主界面' + // let main_ui = getJsonPath('out_domain'); + // // 定义识别对象 + // 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(); + // try { + // let res = captureRegion.find(paimonMenuRo); + // return !res.isEmpty(); + // }finally { + // captureRegion.dispose() + // } + //509, 259, 901, 563 + const text = "退出秘境"; + const ocrRegion = { + x: 509, + y: 259, + w: 901, + h: 563 + } + const find = await findText(text, ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h) + log.debug("识别结果:{1}", find) + return find && find.includes(text) +}; + +/** + * 退出秘境的UI处理函数 + * 该函数用于处理退出秘境界面的相关操作,包括点击确认按钮和检测界面状态 + */ +async function outDomainUI() { + log.info(`{0}`,"退出秘境"); + const ocrRegion = { + x: 509, + y: 259, + w: 901, + h: 563 + } + let ms = 300 + let index = 1 + let tryMax = false + let inMainUI = false + await sleep(ms); + //点击确认按钮 + await findTextAndClick('地脉异常') + await sleep(ms); + while (!await isInOutDomainUI()) { + if (isInMainUI()) { + inMainUI = true + break + } + await sleep(ms); + await keyPress("ESCAPE"); + await sleep(ms * 2); + if (index > 3) { + log.error(`多次尝试匹配退出秘境界面失败 假定已经退出处理`); + tryMax = true + break + } + index += 1 + } + if ((!tryMax) && (!inMainUI) && await isInOutDomainUI()) { + try { + //点击确认按钮 + await findTextAndClick('确认', ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h) + } catch (e) { + // log.error(`多次尝试点击确认失败 假定已经退出处理`); + } + } + + +} + +/** + * 在指定区域内查找文本内容 + * @param {string} text - 要查找的文本内容 + * @param {number} x - 查找区域的左上角x坐标,默认为0 + * @param {number} y - 查找区域的左上角y坐标,默认为0 + * @param {number} w - 查找区域的宽度,默认为1920 + * @param {number} h - 查找区域的高度,默认为1080 + * @param {number} attempts - 尝试查找的次数,默认为5 + * @param {number} interval - 每次尝试之间的间隔时间(毫秒),默认为50 + * @returns {Promise} 返回找到的文本内容,如果未找到则返回空字符串 + */ +async function findText( + text, + x = 0, + y = 0, + w = 1920, + h = 1080, + attempts = 5, + interval = 50, +) { + const keyword = text.toLowerCase(); // 将搜索关键字转换为小写,实现不区分大小写的搜索 + + for (let i = 0; i < attempts; i++) { // 循环尝试查找文本,最多尝试attempts次 + const gameRegion = captureGameRegion(); // 捕获游戏区域图像 + try { + const ro = RecognitionObject.Ocr(x, y, w, h); // 创建OCR识别对象,指定识别区域 + const results = gameRegion.findMulti(ro); // 在区域内查找所有匹配的文本 + + // 遍历查找结果 + for (let j = 0; j < results.count; j++) { + const res = results[j]; + // 检查结果是否存在、包含文本内容,并且文本包含搜索关键字 + if ( + res.isExist() && + res.text && + res.text.toLowerCase().includes(keyword) + ) { + return res.text; // 找到匹配文本,直接返回 + } + } + } finally { + gameRegion.dispose(); // 确保释放游戏区域资源 + } + + await sleep(interval); // 等待指定的时间后进行下一次尝试 + } + + return ""; // 未找到匹配文本,返回空字符串 +} + +/** + * 通用找文本并点击(OCR) + * @param {string} text 目标文本(单个文本) + * @param {number} [x=0] OCR 区域左上角 X + * @param {number} [y=0] OCR 区域左上角 Y + * @param {number} [w=1920] OCR 区域宽度 + * @param {number} [h=1080] OCR 区域高度 + * @param {number} [attempts=5] OCR 尝试次数 + * @param {number} [interval=50] 每次 OCR 之间的等待间隔(毫秒) + * @param {number} [preClickDelay=50] 点击前等待时间(毫秒) + * @param {number} [postClickDelay=50] 点击后等待时间(毫秒) + * + * @returns + * - RecognitionResult | null + */ +async function findTextAndClick( + text, + x = 0, + y = 0, + w = 1920, + h = 1080, + attempts = 5, + interval = 50, + preClickDelay = 50, + postClickDelay = 50 +) { + const keyword = text.toLowerCase(); + + for (let i = 0; i < attempts; i++) { + const gameRegion = captureGameRegion(); + try { + const ro = RecognitionObject.Ocr(x, y, w, h); + const results = gameRegion.findMulti(ro); + + for (let j = 0; j < results.count; j++) { + const res = results[j]; + if ( + res.isExist() && + res.text && + res.text.toLowerCase().includes(keyword) + ) { + await sleep(preClickDelay); + res.click(); + await sleep(postClickDelay); + return res; + } + } + } finally { + gameRegion.dispose(); + } + + await sleep(interval); + } + + return null; +} + +/** + * 抛出错误函数 + * 该函数用于显示错误通知并抛出错误对象 + * @param {string} msg - 错误信息,将用于通知和错误对象 + */ +function throwError(msg, isNotification = false) { + // 使用notification组件显示错误通知 + // notification.error(`${msg}`); + if (isNotification) { + notification.error(`${msg}`); + } + // 抛出一个包含错误信息的Error对象 + throw new Error(`${msg}`); +} + +export { + ocrRegion, + getDayOfWeek, + getJsonPath, + isInMainUI, + toMainUi, + isInOutDomainUI, + outDomainUI, + findTextAndClick, + throwError, +} \ No newline at end of file diff --git a/repo/js/AutoPlan/utils/uid.js b/repo/js/AutoPlan/utils/uid.js new file mode 100644 index 000000000..059f938f6 --- /dev/null +++ b/repo/js/AutoPlan/utils/uid.js @@ -0,0 +1,36 @@ +import {ocrRegion} from './tool.js' + +async function saveOnlyNumber(str) { + str = str ? str : ''; + // 使用正则表达式匹配字符串中的所有数字 + // \d匹配一个或多个数字 + // .join('') 将匹配到的数字数组连接成一个字符串 + // parseInt 将连接后的字符串转换为整数 + // return parseInt(str.match(/\d+/g).join('')); + const matches = str.match(/\d+/g); + if (!matches) { + return 0; // 或抛出错误 + } + return parseInt(matches.join(''), 10); +} + +/** + * OCR识别UID的异步函数 + * 该函数用于通过OCR技术识别屏幕上特定位置的UID文本 + * @returns {Promise} - 异步函数,没有明确的返回值 + */ +async function ocrUid() { + // 定义OCR识别的坐标和尺寸参数 + let uid_json = { + x: 1683, // OCR识别区域的左上角x坐标 + y: 1051, // OCR识别区域的左上角y坐标 + width: 234, // OCR识别区域的宽度 + height: 28, // OCR识别区域的高度 + } + let text = await ocrRegion(uid_json.x, uid_json.y, uid_json.width, uid_json.height); + return await saveOnlyNumber(text); +} + +export { + ocrUid, +}