From 32b950d6a94ad24d933cd6150841e7d2202a5e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=91=E7=AB=AF=E5=AE=A2?= <107686912+Kirito520Asuna@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:57:38 +0800 Subject: [PATCH] =?UTF-8?q?[0.0.3=E7=89=88=E6=9C=AC=E8=BF=AD=E4=BB=A3]--?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=9C=9F=E9=99=90/=E5=91=A8=E6=9C=AC?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=99=A8=20(#2591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(activity): 优化活动筛选逻辑并添加白名单功能 - 新增 relationship 配置项,控制剩余时间与白名单的逻辑关系 - 将 activityNameList 重命名为 whiteActivityNameList 作为白名单功能 - 实现白名单与黑名单的互斥过滤机制 - 更新活动筛选逻辑,支持按剩余时间或白名单条件过滤 - 修改通知标题构建逻辑,显示剩余时间与白名单配置信息 - 更新 README 文档说明白名单与逻辑关系配置使用方法 * chore(ActivitySwitchNotice): 更新版本号 - 将版本号从 0.0.2 更新到 0.0.3 * fix(ActivitySwitchNotice): 修正活动识别日志消息 - 修正了未识别到活动时的日志消息文案,从"未识别到任何活动"改为"不存在符合条件的活动" * refactor: 优化秘境征讨提醒逻辑代码结构 * fix(activity): 修复活动白名单过滤逻辑 - 修复了当配置了关系条件时活动白名单过滤的问题 - 添加了对 config.relationship 的检查以决定是否跳过活动处理 - 确保在没有关系配置的情况下仍然按照白名单过滤活动 * fix(ActivitySwitchNotice): 修复活动白名单逻辑判断错误 - 修正了白名单活动匹配时的关系判断逻辑 - 将 !config.relationship 条件改为 config.relationship - 确保只有在关系配置正确时才跳过非白名单活动 * refactor(ActivitySwitchNotice): 优化OCR识别函数命名 - 将OcrRemainingTime函数重命名为OcrKey以提高通用性 - 更新函数调用以使用新的函数名称 - 保持原有功能逻辑不变,仅优化函数命名规范 * fix(activity): 修复OCR识别和活动过滤逻辑 - 修复OCR识别时资源释放位置,添加try-finally确保截图资源正确释放 - 修改OCR识别逻辑,支持返回多个匹配结果并用'<-->'连接 - 修复活动名称过滤条件中的逻辑运算符空格问题 - 优化代码结构,提高OCR识别的稳定性和准确性 * fix(ActivitySwitchNotice): 修复活动识别结果处理逻辑 - 将 Set 数据结构改为数组并使用 push 方法添加元素 - 保持原有的文本识别和日志记录功能 - 确保返回识别到的文本结果 * feat(ActivitySwitchNotice): 添加根据键名部分内容获取Map值的工具函数 - 新增getMapByKey函数,支持通过键名部分匹配获取Map中的值 - 更新needOcrOtherMap配置,为"飒勇争锋"和"幽境危战"活动添加OCR识别项 - 修改代码逻辑,使用getMapByKey函数替代原有的精确匹配方式 - 优化活动OCR处理流程,提升键名匹配的灵活性 * refactor(ActivitySwitchNotice): 优化OCR识别功能 - 移除"飒勇争锋"活动的OCR配置项 - 修复getMapByKey函数参数格式问题 - 添加函数间空白行以改善代码可读性 - 增强OCR识别日志记录功能,添加命中标识 - 修复变量赋值和参数传递的格式问题 * feat(ActivitySwitchNotice): 添加通用关键词OCR识别功能 - 新增commonList常量定义通用关键词列表 - 实现通用关键词的OCR识别逻辑 - 将识别结果添加到剩余时间文本中 - 支持对已完成等通用状态的自动识别 * feat(ActivitySwitchNotice): 添加通用关键词显示功能 - 在activity.js中添加common变量存储通用关键词 - 将通用关键词信息保存到activityMap中 - 在notice.js中读取并显示通用关键词信息 - 修复通知文本中的格式问题 - 优化条件判断中的空格格式 * docs(ActivitySwitchNotice): 更新README文档添加剩余时间与白名单关系配置说明 - 新增:支持剩余时间和白名单的"与"关系和"或"关系配置说明 - 完善了配置参数文档,增加了`relationship`参数的详细说明 - 更新了版本发布信息和变更日志格式 * feat(ActivitySwitchNotice): 添加剩余时间与白名单关系配置和完成状态显示 - 支持剩余时间和白名单的"与"关系和"或"关系配置 - 添加标记界面显示 `已完成` 的活动功能 - 修复活动过滤逻辑问题,将`activityNameList`更改为`whiteActivityNameList` - 新增黑名单与白名单的互斥过滤机制,黑名单中剔除白名单 - 在配置中增加`relationship`参数,用于控制剩余时间与白名单活动的逻辑关系 * fix(ActivitySwitchNotice): 修复活动切换通知中的逻辑错误和显示问题 - 修复剩余时间白名单关系逻辑,默认从`与`改为`或`关系 - 移除过早的返回语句,确保列表处理逻辑完整执行 - 调整注释格式以保持代码一致性 * fix(ActivitySwitchNotice): 修复活动通用键处理逻辑 - 将common变量初始化为数组而非undefined - 使用push方法将OCR识别文本添加到数组中 - 将数组内容通过逗号连接成字符串存储 - 当数组为空时保持undefined值以维持原有行为 * refactor(Notice): 重构活动通知黑名单文本拼接逻辑 * refactor(ActivitySwitchNotice): 重命名活动学期转换函数并添加文档注释 - 将 getDATE_ENUM 函数重命名为 getActivityTermConversion 以提高语义清晰度 - 为函数添加 JSDoc 注释说明参数和返回值类型 - 在函数内部添加代码注释解释逻辑流程 - 更新函数调用处的函数名称引用 * refactor(ActivitySwitchNotice): 移除废弃的滚动到顶部功能 - 删除了 scrollPagesByActivityToTop 函数的完整实现 - 移除了相关的OCR检测和鼠标滚动逻辑 - 清理了游戏区域截图和资源释放代码 - 保留了优化版的滚动到顶部功能注释 * fix(ActivitySwitchNotice): 修复截图资源释放问题 - 添加 try-finally 块确保 captureRegion 资源正确释放 - 防止截图资源未释放导致的内存泄漏问题 - 保持截图识别和滚动逻辑不变 - 修复黑名单文本格式中的多余空格问题 * refactor(ActivitySwitchNotice): 优化活动列表扫描逻辑的资源管理 - 添加 try-finally 块确保 captureRegion 资源正确释放 - 修复资源泄露问题,避免未调用 dispose() 方法 - 保持原有的活动识别和滚动扫描功能不变 - 优化代码结构提高可读性和维护性 * fix(ocr): 修复OCR文本提取时的空值问题 - 添加了对OCR提取结果的空值检查 - 避免将空值推入common数组中 - 确保只有有效的文本内容才会被添加到数组中 --- repo/js/ActivitySwitchNotice/README.md | 23 +- repo/js/ActivitySwitchNotice/manifest.json | 2 +- repo/js/ActivitySwitchNotice/settings.json | 10 +- .../js/ActivitySwitchNotice/utils/activity.js | 484 ++++++++++-------- .../utils/campaignArea.js | 8 +- repo/js/ActivitySwitchNotice/utils/notice.js | 6 +- 6 files changed, 313 insertions(+), 220 deletions(-) diff --git a/repo/js/ActivitySwitchNotice/README.md b/repo/js/ActivitySwitchNotice/README.md index 90e5d6ba0..8ce01e507 100644 --- a/repo/js/ActivitySwitchNotice/README.md +++ b/repo/js/ActivitySwitchNotice/README.md @@ -36,7 +36,8 @@ | 设置项 | 说明 | 默认值 | 开放 | |:---------------------------|:-----------------------------------------------|:------------|:--:| | `toMainUi` | 执行前是否自动返回游戏主界面 | true | v | -| `activityNameList` | 监控的特定活动名称(用\|分隔) | 空(监控所有活动) | v | +| `relationship` | 剩余时间与白名单启用`和`关系(默认`或`关系) | false | v | +| `whiteActivityNameList` | 白名单活动名称(用\|分隔) | 空(监控所有活动) | v | | `blackActivityNameList` | 黑名单活动名称(用\|分隔) | 空(无黑名单活动) | v | | `notifyHoursThreshold` | 通知时间阈值(小时) | 8760(365天) | v | | `activityKey` | 打开活动页面的快捷键 | F5 | v | @@ -65,7 +66,7 @@ #### 活动筛选 -- **全部活动监控**:`activityNameList` 保持空值,监控所有有剩余时间的活动 +- **全部活动监控**:`whiteActivityNameList` 保持空值,监控所有有剩余时间的活动 - **指定活动监控**:填写活动关键词,如 `海灯节\|盛典`,只监控包含这些关键词的活动 - **黑名单过滤**:`blackActivityNameList` 可以设置不想接收提醒的活动名称,多个活动用`|`分隔 @@ -75,6 +76,11 @@ - 可设置阈值,如设置为24,则只通知剩余时间≤24小时的活动 - 即将结束(24小时内)的活动会在通知中标记 `<即将结束>` +#### 逻辑关系配置 + +- **`relationship` 为 `false`**(默认):满足"剩余时间阈值"或"白名单活动"任一条件即发送通知 +- **`relationship` 为 `true`**:必须同时满足"剩余时间阈值"和"白名单活动"两个条件才发送通知 + #### 智能防重复 - 自动识别已扫描过的活动页面 @@ -122,6 +128,7 @@ + **`以上为用户使用指南全部内容`** --- @@ -141,6 +148,7 @@ ActivitySwitchNotice/ + ## 核心模块 ### `activity.js` - 活动处理核心 @@ -172,7 +180,8 @@ ActivitySwitchNotice/ | 配置项 | 类型 | 说明 | |:---------------------------|:-------:|:--| | `toMainUi` | Boolean | 是否先返回主界面再执行 | -| `activityNameList` | String | 指定活动名称(用\|分隔) | +| `relationship` | Boolean | 剩余时间与白名单启用`和`关系(默认`或`关系) | +| `whiteActivityNameList` | String | 白名单活动名称(用\|分隔) | | `blackActivityNameList` | String | 黑名单活动名称(用\|分隔) | | `notifyHoursThreshold` | Number | 通知阈值(小时) | | `activityKey` | String | 打开活动页面的快捷键 | @@ -203,6 +212,14 @@ ActivitySwitchNotice/ ## 版本历史 +### 0.0.3 (2025-12-29) + +- 修复 修复了活动过滤逻辑问题,将`activityNameList`更改为`whiteActivityNameList`以保持一致 +- 新增 黑名单与白名单的互斥过滤机制,黑名单中剔除白名单 +- 新增 在配置中增加了`relationship`参数,用于控制剩余时间与白名单活动的逻辑关系 +- 新增 支持剩余时间和白名单的"与"关系和"或"关系配置 +- 新增 标记界面显示 `已完成` 的活动 + ### 0.0.2 (2025-12-22) - 新增 征讨领域周次数提醒功能 diff --git a/repo/js/ActivitySwitchNotice/manifest.json b/repo/js/ActivitySwitchNotice/manifest.json index 36a15f237..1d1985840 100644 --- a/repo/js/ActivitySwitchNotice/manifest.json +++ b/repo/js/ActivitySwitchNotice/manifest.json @@ -1,6 +1,6 @@ { "name": "活动期限/周本通知器", - "version": "0.0.2", + "version": "0.0.3", "description": "", "settings_ui": "settings.json", "main": "main.js", diff --git a/repo/js/ActivitySwitchNotice/settings.json b/repo/js/ActivitySwitchNotice/settings.json index 289b46f59..c8fd7ce9d 100644 --- a/repo/js/ActivitySwitchNotice/settings.json +++ b/repo/js/ActivitySwitchNotice/settings.json @@ -6,9 +6,15 @@ "default": true }, { - "name": "activityNameList", + "name": "relationship", + "type": "checkbox", + "label": "剩余时间,白名单 启用`和`关系(默认`或`关系)", + "default": false + }, + { + "name": "whiteActivityNameList", "type": "input-text", - "label": "活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>" + "label": "白名单活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>" }, { "name": "blackActivityNameList", diff --git a/repo/js/ActivitySwitchNotice/utils/activity.js b/repo/js/ActivitySwitchNotice/utils/activity.js index 1d56832a4..69c25bd11 100644 --- a/repo/js/ActivitySwitchNotice/utils/activity.js +++ b/repo/js/ActivitySwitchNotice/utils/activity.js @@ -8,12 +8,20 @@ function settingsParseInt(str, defaultValue) { } const config = { - activityNameList: (settings.activityNameList ? settings.activityNameList.split('|') : []), + //剩余时间,白名单 启用`和`关系(默认`与`关系) + relationship: settings.relationship, + whiteActivityNameList: (settings.whiteActivityNameList ? settings.whiteActivityNameList.split('|').filter(s => s.trim()) : []), activityKey: (settings.activityKey ? settings.activityKey : 'F5'), toTopCount: settingsParseInt(settings.toTopCount, 10),//滑动到顶最大尝试次数 scrollPageCount: settingsParseInt(settings.scrollPageCount, 4),//滑动次数/页 notifyHoursThreshold: settingsParseInt(settings.notifyHoursThreshold, 8760),//剩余时间阈值(默认 8760小时=365天) - blackActivityNameList: (settings.blackActivityNameList ? settings.blackActivityNameList.split('|').filter(s => s.trim()) : []),//黑名单活动名称 + // 黑名单活动名称列表,这些活动将被排除在识别和处理之外 + // 通过 | 分隔多个活动名称,并过滤掉空白项 + // 同时确保黑名单中的活动名称不包含在白名单(whiteActivityNameList)中 + blackActivityNameList: (settings.blackActivityNameList ? settings.blackActivityNameList.split('|').filter(s => s.trim()) + .filter( + item => !settings.whiteActivityNameList.split('|').filter(s => s.trim()).some(keyword => item.includes(keyword)) + ) : []), } const ocrRegionConfig = { activity: {x: 267, y: 197, width: 226, height: 616},//活动识别区域坐标和尺寸 @@ -37,20 +45,48 @@ const DATE_ENUM = Object.freeze({ const activityTermConversionMap = new Map([ ["砺行修远", {dateEnum: DATE_ENUM.WEEK}], ]); +const commonList = ["已完成"] const needOcrOtherMap = new Map([ ["砺行修远", ["本周进度", "完成进度"]], + ["幽境危战", ["紊乱爆发期"]], ]); const genshinJson = { width: 1920,//genshin.width, height: 1080,//genshin.height, } +/** + * 根据键名的一部分内容从Map中获取对应的值 + * 遍历Map的所有键名,查找包含指定key字符串的键,并返回对应的值 + * + * @param {Map} map - 要搜索的Map对象,默认为空Map + * @param {string} key - 用于匹配的键名部分字符串 + * @returns {*} 匹配键对应的值,如果未找到匹配项则返回undefined + */ +function getMapByKey(map = new Map(), key) { + // 遍历Map的所有键名,查找包含指定key的键 + for (let keyName of map.keys()) { + if (keyName.includes(key)) { + return map.get(keyName) + } + } + return undefined +} -function getDATE_ENUM(activityName) { +/** + * 根据活动名称获取对应的学期转换规则 + * @param {string} activityName - 活动名称 + * @returns {object} 返回包含dateEnum属性的对象,表示日期枚举类型 + */ +function getActivityTermConversion(activityName) { + // 遍历活动学期转换映射表的所有键 for (let key of activityTermConversionMap.keys()) { + // 检查活动名称是否包含当前键 if (activityName.includes(key)) + // 如果包含,则返回映射表中对应的值 return activityTermConversionMap.get(key) } + // 如果没有匹配到任何键,则返回默认的小时日期枚举 return {dateEnum: DATE_ENUM.HOUR} } @@ -96,7 +132,7 @@ async function scrollPage(totalDistance, isUp = false, waitCount = 6, stepDistan } /** - * 根据活动状态进行页面滚动 + * 根据活动状态 进行页面滚动 * @param {boolean} isUp - 是否向上滚动,默认为false */ async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30) { @@ -117,41 +153,6 @@ async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, st } } -/** - * 通过滚动页面直到到达顶部位置 - * 该函数会持续滚动页面,直到检测到页面顶部的标识不再变化为止 - * @returns {Promise} 无返回值,当到达顶部时函数执行结束 - * @throws {Error} 如果尝试滚动超过10次仍未到达顶部,抛出错误 - */ -async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) { - let topName = null // 用于存储检测到的顶部标识文本 - let index = 0 // 记录滚动尝试次数的计数器 - // 无限循环,直到到达顶部后通过return退出 - while (true) { - // 检查是否已超过最大尝试次数(10次) - if (index >= config.toTopCount) { - throw new Error("回到顶部失败") // 超过尝试次数抛出错误 - } - await moveMouseTo(0, 20) - index++ // 增加尝试次数计数器 - let captureRegion = captureGameRegion(); // 获取游戏区域截图 - const ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); // 创建OCR识别对象 - - // ocrObject.threshold = 1.0; - let resList = captureRegion.findMulti(ocrObject); // 在指定区域进行OCR识别 - captureRegion.dispose(); // 释放截图资源 - if (topName !== resList[0].text) { - topName = resList[0].text - } else { - log.info(`回到顶部成功`) - // break - return - } - await scrollPagesByActivity(true, 80 * 4, 6, 60) - } - -} - /** * 滚动到活动页面最顶部(优化版) * 通过连续检测顶部活动名称相同来确认已到顶,更加健壮 @@ -176,56 +177,65 @@ async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) await moveMouseTo(0, 20); // 截图 + OCR 识别活动列表区域 - let captureRegion = captureGameRegion(); - const ocrObject = RecognitionObject.Ocr( - ocrRegion.x, - ocrRegion.y, - ocrRegion.width, - ocrRegion.height - ); - // 可选:提升识别率 - // ocrObject.threshold = 0.8; + let captureRegion = null; + try { + captureRegion = captureGameRegion(); + const ocrObject = RecognitionObject.Ocr( + ocrRegion.x, + ocrRegion.y, + ocrRegion.width, + ocrRegion.height + ); + // 可选:提升识别率 + // ocrObject.threshold = 0.8; - let resList = captureRegion.findMulti(ocrObject); - captureRegion.dispose(); + let resList = captureRegion.findMulti(ocrObject); + // captureRegion.dispose(); - // 如果完全没识别到任何活动,可能是页面异常或已在顶(极少情况) - if (resList.length === 0) { - log.warn("顶部OCR未识别到任何活动条目,可能是页面为空或识别失败"); - // 再尝试一次向上滚大距离 - // await scrollPagesByActivity(true); // true = 向上 - await scrollPagesByActivity(true, 80 * 4, 6, 60); - await sleep(ms); - continue; - } - - // 取当前识别到的最顶部活动名称(resList[0] 通常是列表最上面的) - const currentTopName = resList[0].text.trim(); - - log.info(`当前检测到的顶部活动: {currentTopName}`, currentTopName); - - // 判断是否与上一次相同 - if (currentTopName === topActivityName) { - sameTopCount++; - log.debug(`顶部活动连续相同 ${sameTopCount} 次`); - - if (sameTopCount >= requiredSameCount) { - log.info(`已连续 {sameTopCount} 次检测到相同顶部活动,确认回到页面最顶部!`, sameTopCount); - return; // 成功回到顶部 + // 如果完全没识别到任何活动,可能是页面异常或已在顶(极少情况) + if (resList.length === 0) { + log.warn("顶部OCR未识别到任何活动条目,可能是页面为空或识别失败"); + // 再尝试一次向上滚大距离 + // await scrollPagesByActivity(true); // true = 向上 + await scrollPagesByActivity(true, 80 * 4, 6, 60); + await sleep(ms); + continue; + } + + // 取当前识别到的最顶部活动名称(resList[0] 通常是列表最上面的) + const currentTopName = resList[0].text.trim(); + + log.info(`当前检测到的顶部活动: {currentTopName}`, currentTopName); + + // 判断是否与上一次相同 + if (currentTopName === topActivityName) { + sameTopCount++; + log.debug(`顶部活动连续相同 ${sameTopCount} 次`); + + if (sameTopCount >= requiredSameCount) { + log.info(`已连续 {sameTopCount} 次检测到相同顶部活动,确认回到页面最顶部!`, sameTopCount); + return; // 成功回到顶部 + } + } else { + // 顶部名称变了,说明还在向上滚动,重置计数 + topActivityName = currentTopName; + sameTopCount = 1; // 这次算第一次 + } + + // 未达到稳定状态,继续向上滚动一页(可根据实际情况调整滚动距离) + // 这里使用更大滚动距离确保能快速回顶 + // await scrollPagesByActivity(true); // true = 向上 + // 可选:加大单次滚动量(如果你发现默认一页不够) + await scrollPagesByActivity(true, 80 * 4, 6, 60); + + await sleep(ms); // 给页面滚动和渲染留时间 + } finally { + // 确保资源被正确释放 + if (captureRegion) { + captureRegion.dispose(); } - } else { - // 顶部名称变了,说明还在向上滚动,重置计数 - topActivityName = currentTopName; - sameTopCount = 1; // 这次算第一次 } - // 未达到稳定状态,继续向上滚动一页(可根据实际情况调整滚动距离) - // 这里使用更大滚动距离确保能快速回顶 - // await scrollPagesByActivity(true); // true = 向上 - // 可选:加大单次滚动量(如果你发现默认一页不够) - await scrollPagesByActivity(true, 80 * 4, 6, 60); - - await sleep(ms); // 给页面滚动和渲染留时间 } // 超过最大尝试次数仍未稳定 @@ -322,20 +332,31 @@ function convertHoursToWeeksDaysHours(totalHours) { * @param {string} key - 要识别的关键词,默认为"剩余时间" * @returns {string|null} 返回识别到的剩余时间文本,若未识别到则返回null */ -async function OcrRemainingTime(activityName, key = "剩余时间", ocrRegion = ocrRegionConfig.remainingTime) { +async function OcrKey(activityName, key = "剩余时间", ocrRegion = ocrRegionConfig.remainingTime) { + let captureRegion = captureGameRegion(); // 获取游戏区域截图 - const ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); // 创建OCR识别对象 - // ocrObject.threshold = 1.0; - let resList = captureRegion.findMulti(ocrObject); // 在指定区域进行OCR识别 - captureRegion.dispose(); // 释放截图资源 - for (let res of resList) { - if (res.text.includes(key)) { // 检查识别结果是否包含关键词 - log.debug(`{activityName}--{time}`, activityName, res.text); // 记录日志 - return res.text // 返回识别到的文本 + try { + let list = new Array() + const ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); // 创建OCR识别对象 + // ocrObject.threshold = 1.0; + let resList = captureRegion.findMulti(ocrObject); // 在指定区域进行OCR识别 + for (let res of resList) { + log.debug(`[info][{key}]{activityName}--{time}`, key, activityName, res.text); // 记录日志 + if (res.text.includes(key)) { // 检查识别结果是否包含关键词 + log.debug(`[{key}][命中]{activityName}--{time}`, key, activityName, res.text); // 记录日志 + list.push(res.text.trim()) + // return res.text // 返回识别到的文本 + } } + if (list.length > 0) { + return list.join('<-->') + } + // 没有识别到剩余时间 + return null; + + } finally { + captureRegion.dispose(); // 释放截图资源 } - // 没有识别到剩余时间 - return null; } @@ -375,149 +396,196 @@ async function activityMain() { // 移动鼠标到安全位置,避免干扰截图 await moveMouseTo(0, 20); // 获取当前页面活动列表区域截图并 OCR - let captureRegion = captureGameRegion(); - const ocrObject = RecognitionObject.Ocr( - ocrRegionConfig.activity.x, - ocrRegionConfig.activity.y, - ocrRegionConfig.activity.width, - ocrRegionConfig.activity.height - ); - let resList = captureRegion.findMulti(ocrObject); - captureRegion.dispose(); + let captureRegion = null; + try { + captureRegion = captureGameRegion(); - // 如果本页完全没有识别到活动,可能是到底了或 OCR 失败 - if (resList.length === 0) { - log.info("当前页未识别到任何活动,视为已到页面底部"); - break; - } - // ============ 新增:提前判断是否为重复页 ============ - const currentPageNames = new Set(); - for (let res of resList) { - currentPageNames.add(res.text.trim()); - } + const ocrObject = RecognitionObject.Ocr( + ocrRegionConfig.activity.x, + ocrRegionConfig.activity.y, + ocrRegionConfig.activity.width, + ocrRegionConfig.activity.height + ); + let resList = captureRegion.findMulti(ocrObject); + // captureRegion.dispose(); - // 计算与上一页的重合率 - if (previousPageActivities.size > 0) { - let overlapCount = 0; - for (let name of currentPageNames) { - if (previousPageActivities.has(name)) overlapCount++; - } - const overlapRatio = overlapCount / previousPageActivities.size; - - // 如果重合率 >= 70%(可调整),认为滚动未生效,是重复页 - if (overlapRatio >= 0.7) { - log.info(`检测到当前页与上一页高度重复(重合率 ${Math.round(overlapRatio * 100)}%),已到达底部,停止扫描`); + // 如果本页完全没有识别到活动,可能是到底了或 OCR 失败 + if (resList.length === 0) { + log.info("当前页未识别到任何活动,视为已到页面底部"); break; } - } + // ============ 新增:提前判断是否为重复页 ============ + const currentPageNames = new Set(); + for (let res of resList) { + currentPageNames.add(res.text.trim()); + } - // 更新上一页记录(为下一轮做准备) - previousPageActivities = currentPageNames; - // ================================================= + // 计算与上一页的重合率 + if (previousPageActivities.size > 0) { + let overlapCount = 0; + for (let name of currentPageNames) { + if (previousPageActivities.has(name)) overlapCount++; + } + const overlapRatio = overlapCount / previousPageActivities.size; - let currentPageBottomName = null; // 本页最下面的活动名 - let newActivityCountThisPage = 0; - - // 遍历当前页所有识别到的活动条目 - for (let res of resList) { - const activityName = res.text.trim(); - - // 如果设置了指定活动列表,只处理包含这些关键词的活动 - if (config.activityNameList.length > 0) { - const matched = config.activityNameList.some(keyword => activityName.includes(keyword)); - if (!matched) { - continue; // 不关心的活动,跳过不点击 + // 如果重合率 >= 70%(可调整),认为滚动未生效,是重复页 + if (overlapRatio >= 0.7) { + log.info(`检测到当前页与上一页高度重复(重合率 ${Math.round(overlapRatio * 100)}%),已到达底部,停止扫描`); + break; } } - if (config.blackActivityNameList.length > 0) { - const matched = config.blackActivityNameList.some(keyword => activityName.includes(keyword)); - if (matched) { - continue; // 不关心的活动,跳过不点击 + // 更新上一页记录(为下一轮做准备) + previousPageActivities = currentPageNames; + // ================================================= + + let currentPageBottomName = null; // 本页最下面的活动名 + let newActivityCountThisPage = 0; + + // 遍历当前页所有识别到的活动条目 + for (let res of resList) { + const activityName = res.text.trim(); + + // 如果设置了指定活动列表,只处理包含这些关键词的活动 + if (config.whiteActivityNameList.length > 0) { + const matched = config.whiteActivityNameList.some(keyword => activityName.includes(keyword)); + if (!matched && (config.relationship)) { + continue; // 不关心的活动,跳过不点击 + } } - } - // 避免重复点击同一个活动(防止 OCR 误识别或页面抖动) - if (activityMap.has(activityName)) { - log.info(`活动已记录,跳过重复点击: ${activityName}`); - } else { - await click(res.x, res.y); // 点击进入活动详情 - await sleep(ms); - - let remainingTimeText = await OcrRemainingTime(activityName); - if (remainingTimeText) { - const totalHours = parseRemainingTimeToHours(remainingTimeText); - if (totalHours <= 24 && totalHours > 0) { - remainingTimeText += '<即将结束>' + if (config.blackActivityNameList.length > 0) { + const matched = config.blackActivityNameList.some(keyword => activityName.includes(keyword)); + if (matched) { + continue; // 不关心的活动,跳过不点击 } - let desc = "" + } - let dateEnum = getDATE_ENUM(activityName); - log.debug(`activityName:{activityName},dateEnum:{dateenum.dateEnum}`, activityName, dateEnum.dateEnum) - switch (dateEnum.dateEnum) { - case DATE_ENUM.WEEK: - desc += "|==>" + convertHoursToWeeksDaysHours(totalHours) + "<==|"; - break; - case DATE_ENUM.HOUR: - break; - default: - break; - } - if (needOcrOtherMap.has(activityName)) { - const keys = needOcrOtherMap.get(activityName); - for (const key of keys) { - let text = await OcrRemainingTime(activityName, key); - if (text) { - remainingTimeText += " [" + text + "] " + // 避免重复点击同一个活动(防止 OCR 误识别或页面抖动) + if (activityMap.has(activityName)) { + log.info(`活动已记录,跳过重复点击: ${activityName}`); + } else { + await click(res.x, res.y); // 点击进入活动详情 + await sleep(ms); + + let remainingTimeText = await OcrKey(activityName); + if (remainingTimeText) { + const totalHours = parseRemainingTimeToHours(remainingTimeText); + if (totalHours <= 24 && totalHours > 0) { + remainingTimeText += '<即将结束>' + } + let desc = "" + + let dateEnum = getActivityTermConversion(activityName); + log.debug(`activityName:{activityName},dateEnum:{dateenum.dateEnum}`, activityName, dateEnum.dateEnum) + switch (dateEnum.dateEnum) { + case DATE_ENUM.WEEK: + desc += "|==>" + convertHoursToWeeksDaysHours(totalHours) + "<==|"; + break; + case DATE_ENUM.HOUR: + break; + default: + break; + } + let needMap = getMapByKey(needOcrOtherMap, activityName); + if (needMap) { + const keys = needMap; + for (const key of keys) { + let text = await OcrKey(activityName, key); + if (text) { + remainingTimeText += " [" + text + "] " + } } } + let common = new Array() + // 通用key + if (commonList && commonList.length > 0) { + for (let commonKey of commonList) { + let text = await OcrKey(activityName, commonKey); + if (text) { + common.push(text) + } + } + } + activityMap.set(activityName, { + common: common.length > 0 ? common.join(",") : undefined, + text: remainingTimeText, + hours: totalHours, + desc: desc + }); + log.info(`成功记录 → {activityName} {remainingTime} 共计: {hours} 小时`, activityName, remainingTimeText, totalHours); + newActivityCountThisPage++; } - activityMap.set(activityName, { - text: remainingTimeText, - hours: totalHours, - desc: desc - }); - log.info(`成功记录 → {activityName} {remainingTime} 共计: {hours} 小时`, activityName, remainingTimeText, totalHours); - newActivityCountThisPage++; + + await sleep(ms); } - await sleep(ms); + // 更新本页最下面的活动名 + currentPageBottomName = activityName; } - - // 更新本页最下面的活动名 - currentPageBottomName = activityName; - } - // 备用判断:本页一个新活动都没加,也认为到底(双保险) - if (newActivityCountThisPage === 0 && scannedPages > 1) { - log.info("本页无新活动添加,确认已到底"); - break; - } - // 5. 判断是否已到达页面底部 - if (currentPageBottomName && currentPageBottomName === lastPageBottomName) { - sameBottomCount++; - if (sameBottomCount >= sameBottomCountMax) { - log.info("连续{sameBottomCountMax}次检测到相同底部活动,已确认到达页面最底部,扫描结束", sameBottomCountMax); + // 备用判断:本页一个新活动都没加,也认为到底(双保险) + if (newActivityCountThisPage === 0 && scannedPages > 1) { + log.info("本页无新活动添加,确认已到底"); break; } - } else { - sameBottomCount = 0; // 重置计数 - } - lastPageBottomName = currentPageBottomName; + // 5. 判断是否已到达页面底部 + if (currentPageBottomName && currentPageBottomName === lastPageBottomName) { + sameBottomCount++; + if (sameBottomCount >= sameBottomCountMax) { + log.info("连续{sameBottomCountMax}次检测到相同底部活动,已确认到达页面最底部,扫描结束", sameBottomCountMax); + break; + } + } else { + sameBottomCount = 0; // 重置计数 + } + lastPageBottomName = currentPageBottomName; - // 6. 向下滑动一页,继续下一轮 - await scrollPagesByActivity(false); // false = 向下滚动 - await sleep(ms); + // 6. 向下滑动一页,继续下一轮 + await scrollPagesByActivity(false); // false = 向下滚动 + await sleep(ms); + } finally { + if (captureRegion) { + captureRegion.dispose(); + } + } } let activityMapFilter = new Map(); Array.from(activityMap.entries()) - .filter(([name, info]) => info.hours <= config.notifyHoursThreshold) + .filter(([name, info]) => { + // 检查活动是否满足通知条件: + // 1. 剩余时间小于等于阈值 + // 2. 活动名称包含在白名单中 + const isWithinThreshold = info.hours <= config.notifyHoursThreshold; + const isInWhitelist = config.whiteActivityNameList.some(keyword => name.includes(keyword)); + return config.relationship ? (isWithinThreshold && isInWhitelist) : (isWithinThreshold || isInWhitelist); + }) .forEach(([name, info]) => activityMapFilter.set(name, info)); + + if (config.whiteActivityNameList.length > 0) { + log.info(`[模式]==>(剩余时间,白名单)已开启{key}模式`, config.relationship ? `和` : `或`) + + } // 7. 全部扫描完毕,统一发送通知(只发一次!) if (activityMapFilter.size > 0) { log.info(`扫描完成,共记录 {activityMap.size} 个活动,即将发送通知`, activityMapFilter.size); - await noticeUtil.sendNotice(activityMapFilter, `原神活动剩余时间提醒(仅显示剩余 ≤ ${config.notifyHoursThreshold} 小时的活动)${config.blackActivityNameList.length <= 0 ? "" : "|==>已开启黑名单:" + config.blackActivityNameList.join(",") + "<==|"}`); + // 构建通知标题,根据配置显示剩余时间阈值和白名单活动信息 + let titleKey = `[ `; + titleKey += `剩余时间 <= ${config.notifyHoursThreshold} 小时`; + // 如果配置了白名单活动,则在标题中添加相关信息 + if (config.whiteActivityNameList.length > 0) { + titleKey += config.relationship ? ` 和 ` : ` 或 `; + titleKey += `白名单 <<${config.whiteActivityNameList.join(", ")}>>`; + } + titleKey += `] `; + + let blackText = ""; + if (config.blackActivityNameList.length > 0) { + blackText += `|==>已开启黑名单: ${config.blackActivityNameList.join(",")}<==|` + } + + await noticeUtil.sendNotice(activityMapFilter, `原神活动剩余时间提醒(仅显示 ${titleKey} 的活动)${blackText}`); } else { - log.warn("未识别到任何活动,未发送通知"); + log.warn("不存在符合条件的活动,未发送通知"); } } diff --git a/repo/js/ActivitySwitchNotice/utils/campaignArea.js b/repo/js/ActivitySwitchNotice/utils/campaignArea.js index f2216f7c6..541d30667 100644 --- a/repo/js/ActivitySwitchNotice/utils/campaignArea.js +++ b/repo/js/ActivitySwitchNotice/utils/campaignArea.js @@ -86,12 +86,12 @@ async function campaignAreaMain() { // 获取当前星期信息 let dayOfWeek = await getDayOfWeek(); // 如果不是周日(0代表周日),则直接返回 - if (dayOfWeek.day != config.campaignAreaReminderDay) { - log.info(`[{dayOfWeek.dayOfWeek}],跳过执行秘境征讨剩余次数提醒`, dayOfWeek.dayOfWeek) + const bool = dayOfWeek.day != config.campaignAreaReminderDay; + // 记录开始执行秘境征讨提醒的日志 + log.info(`[{dayOfWeek.dayOfWeek}],${bool?"跳过":"开始"}执行秘境征讨剩余次数提醒`, dayOfWeek.dayOfWeek) + if (bool) { return } - // 记录开始执行秘境征讨提醒的日志 - log.info(`[{dayOfWeek.dayOfWeek}],开始执行秘境征讨剩余次数提醒`, dayOfWeek.dayOfWeek) // 设置操作间隔时间(毫秒) let ms = 600 // 等待一段时间 diff --git a/repo/js/ActivitySwitchNotice/utils/notice.js b/repo/js/ActivitySwitchNotice/utils/notice.js index 2637aa166..a6cc277aa 100644 --- a/repo/js/ActivitySwitchNotice/utils/notice.js +++ b/repo/js/ActivitySwitchNotice/utils/notice.js @@ -16,7 +16,9 @@ async function sendNotice(map, title, noNotice) { let noticeText = title ? title + "\n======\n" : "\n" for (const [name, info] of sortedEntries) { - noticeText += `> ${name} ${info.text} (还剩 ${info.hours} 小时) ${info.desc}\n----\n`; + let common = info.common + common = common ? `(${common})` : '' + noticeText += `> ${common} ${name} ${info.text} (还剩 ${info.hours} 小时) ${info.desc}\n----\n`; } // 发送通知 notification.send(noticeText) @@ -30,7 +32,7 @@ async function sendNotice(map, title, noNotice) { */ async function send(noticeText, title, noNotice) { // 检查是否有通知内容且设置了不发送通知的标志 - if (noticeText&&noNotice) { + if (noticeText && noNotice) { log.info(`无通知内容`) // 记录日志信息 return // 直接返回,不执行后续操作 }