From 37ff6a6e5b81be44b31f5e4a9aeeeb1e0e054a16 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: Sun, 4 Jan 2026 11:04:59 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=900.0.5=E3=80=91=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E6=9C=9F=E9=99=90/=E5=91=A8=E6=9C=AC=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=99=A8=20(#2618)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ActivitySwitchNotice): 支持解析包含分钟的时间文本 - 添加对分钟单位的解析支持,使用正则表达式匹配"分钟" - 将天数、小时数、分钟数都改为浮点数解析以支持小数 - 添加数值非负验证,确保解析结果不为负数 - 将分钟转换为小时进行统一计算 - 对结果进行四舍五入取整 - 修复OCR识别结果不完整的问题,自动补全"小时"和"分钟"单位 * feat(activity): 添加活动黑名单条件过滤功能 - 新增 parseWhiteActivity 和 parseBlackActivity 函数,支持解析黑白名单格式 - 实现黑名单条件匹配机制,支持活动名-条件1,条件2的语法格式 - 添加 getMapByKey 函数支持反向匹配功能 - 更新配置初始化流程,添加 init 函数处理黑名单配置 - 改进活动过滤逻辑,支持条件匹配检查 - 更新 README.md 文档说明新的黑名单条件语法 - 修改设置项名称从 blackActivityNameList 到 blackActivity - 优化配置项标签说明,添加条件语法使用说明 - 添加版本历史记录 0.0.5 版本更新内容 * refactor: 重构黑名单过滤机制,新增条件匹配功能 * feat(ActivitySwitchNotice): 添加manifest.json读取和版本日志功能 - 添加manifest.json文件读取功能 - 将初始化日志从info级别调整为debug级别 - 在启动时输出版本信息日志 - 初始化manifest变量以存储应用清单数据 * chore(docs): 更新版本发布日期 - 修正版本 0.0.5 的发布日期从 2026-01-03 为 2026-01-04 * chore(ActivitySwitchNotice): 更新版本号并清理manifest配置 - 将版本号从 0.0.4 更新到 0.0.5 - 移除了 http_allowed_urls 中多余的逗号,修复了JSON格式 - 保持了原有的依赖配置和URL访问权限设置 * fix(ActivitySwitchNotice): 修复OCR键函数调用参数缺失问题 - 修复OcrKey函数调用时缺少activityName参数的问题 - 确保黑名单条件检查时传递正确的活动名称参数 - 解决因参数缺失可能导致的条件匹配错误 * docs(ActivitySwitchNotice): 更新README文档添加逻辑流程图 - 添加了详细的逻辑流程说明 - 使用mermaid图表展示组件间交互流程 - 补充了配置初始化到活动过滤的完整流程 - 说明了黑名单匹配和OCR校验的具体逻辑 - 更新了用户使用指南章节结构 * docs(ActivitySwitchNotice): 更新活动过滤器流程图文档 - 修正了活动过滤器流程图中的条件判断逻辑描述 - 更新了黑名单匹配条件的处理流程说明 --- repo/js/ActivitySwitchNotice/README.md | 97 ++++++++- repo/js/ActivitySwitchNotice/main.js | 5 +- repo/js/ActivitySwitchNotice/manifest.json | 4 +- repo/js/ActivitySwitchNotice/settings.json | 4 +- .../js/ActivitySwitchNotice/utils/activity.js | 184 +++++++++++++++--- 5 files changed, 250 insertions(+), 44 deletions(-) diff --git a/repo/js/ActivitySwitchNotice/README.md b/repo/js/ActivitySwitchNotice/README.md index 05216eb82..0ee2ce58b 100644 --- a/repo/js/ActivitySwitchNotice/README.md +++ b/repo/js/ActivitySwitchNotice/README.md @@ -14,13 +14,77 @@ - ✅ 智能解析剩余时间(支持"22天14小时"等格式) - ✅ 可配置的通知阈值(默认8760小时内结束的活动) - ✅ 支持指定特定活动进行监控 -- ✅ 支持活动黑名单过滤功能 +- ✅ 支持活动黑名单过滤功能(0.0.5版本,新增支持为黑名单活动设置特定条件,只有满足条件时才过滤,注:特定条件为空默认没有条件) - ✅ 防重复检测机制 - ✅ 异常处理和错误恢复 - ✅ 自动提醒征讨领域减半剩余次数(默认`周日`提醒可配置) - ✅ 支持独立通知功能(`0.0.4`版本新增 因BGI不支持WebSocket,需搭配WsProxy+开启JS HTTP 权限使用)[前往WsProxy部署](https://github.com/Kirito520Asuna/WsProxy) +## 逻辑流程 +```mermaid +sequenceDiagram + autonumber + participant Config as 配置初始化 + participant Parser as 解析器 + participant ActivityMgr as 活动管理 + participant OCRSvc as OCR 服务 + participant Filter as 过滤决策 + participant Notification as 通知服务 + Config->>Parser: 读取 settings.whiteActivityNameList + Parser->>Parser: parseWhiteActivity(text) + Parser-->>Config: 返回 whiteActivityNameList + + Config->>Parser: 读取 settings.blackActivity + Parser->>Parser: parseBlackActivity(text, excludeList) + Parser-->>Config: 返回 blackActivityMap + + Note over ActivityMgr: 遍历所有候选活动 + loop 每个候选活动 + Note over ActivityMgr: 检查白名单匹配 + alt 白名单不为空 + ActivityMgr->>ActivityMgr: 检查活动名是否包含白名单关键词 + alt 包含关键词 + ActivityMgr-->>ActivityMgr: 标记为白名单活动 + else 不包含关键词 + alt relationship为false(或关系) + ActivityMgr-->>ActivityMgr: 继续处理 + else relationship为true(与关系) + ActivityMgr-->>ActivityMgr: 跳过该活动 + end + end + end + + Note over ActivityMgr: 检查黑名单匹配 + alt 黑名单不为空 + ActivityMgr->>Filter: 是否匹配黑名单? + Filter->>Parser: getMapByKey(blackActivityMap, 活动名, reverseMatch=true) + Parser-->>Filter: 返回条件列表 + Filter->>OCRSvc: 用 OCR 校验条件(如剩余时间、文本等) + OCRSvc-->>Filter: 返回条件满足情况 + alt 任一条件满足 + Filter-->>ActivityMgr: 跳过该活动 + else 所有条件均不满足 + Filter-->>ActivityMgr: 保留该活动 + end + else 匹配但无条件 + Filter-->>ActivityMgr: 直接跳过该活动 + else 未匹配黑名单 + Filter-->>ActivityMgr: 保留该活动 + end + end + + Note over ActivityMgr: 活动筛选完成后 + ActivityMgr->>ActivityMgr: 根据whiteActivityNameList和relationship进行二次筛选 + alt relationship为true(与关系) + ActivityMgr->>ActivityMgr: 同时满足剩余时间阈值和白名单条件 + else relationship为false(或关系) + ActivityMgr->>ActivityMgr: 满足剩余时间阈值或白名单任一条件 + end + ActivityMgr->>ActivityMgr: 检查剩余时间阈值 + ActivityMgr->>Notification: 发送符合条件的活动通知 + Notification-->> ActivityMgr: 通知发送完成 +``` --- ## 用户使用指南 @@ -43,7 +107,7 @@ | `noticeType` | 通知模式(默认BGI通知-使用独立通知需要开启JS HTTP权限) | BGI通知 | v | | `relationship` | 剩余时间与白名单启用`和`关系(默认`或`关系) | false | v | | `whiteActivityNameList` | 白名单活动名称(用\|分隔) | 空(监控所有活动) | v | -| `blackActivityNameList` | 黑名单活动名称(用\|分隔) | 空(无黑名单活动) | v | +| `blackActivity` | 黑名单活动名称(用|分隔)- 支持条件语法:活动名-条件1,条件2 | 空(无黑名单活动) | v | | `notifyHoursThreshold` | 通知时间阈值(小时) | 8760(365天) | v | | `activityKey` | 打开活动页面的快捷键 | F5 | v | | `toTopCount` | 滑动到顶最大尝试次数 | 10 | x | @@ -83,7 +147,15 @@ - **全部活动监控**:`whiteActivityNameList` 保持空值,监控所有有剩余时间的活动 - **指定活动监控**:填写活动关键词,如 `海灯节\|盛典`,只监控包含这些关键词的活动 -- **黑名单过滤**:`blackActivityNameList` 可以设置不想接收提醒的活动名称,多个活动用`|`分隔 +- **黑名单过滤**:blackActivity 可以设置不想接收提醒的活动名称,多个活动用|分隔 +- **条件黑名单过滤**:支持条件语法 活动名-条件1,条件2,只有当活动满足指定条件时才过滤 + +##### 使用示例 +```text +普通黑名单: "活动A|活动B" +条件黑名单: "活动A-已完成|活动B-条件1,条件2" +混合使用: "活动A|活动C-已完成,已领取" +``` #### 时间通知机制 @@ -270,7 +342,7 @@ ActivitySwitchNotice/ | `noticeType` | String | 通知模式(BGI通知/独立通知/两者都发送) | | `relationship` | Boolean | 剩余时间与白名单启用`和`关系(默认`或`关系) | | `whiteActivityNameList` | String | 白名单活动名称(用\|分隔) | -| `blackActivityNameList` | String | 黑名单活动名称(用\|分隔) | +| `blackActivity` | String | 黑名单活动名称(用\|分隔)- 支持条件语法:活动名-条件1,条件2 | | `notifyHoursThreshold` | Number | 通知阈值(小时) | | `activityKey` | String | 打开活动页面的快捷键 | | `toTopCount` | Number | 滑动到顶最大尝试次数 | @@ -280,9 +352,9 @@ ActivitySwitchNotice/ | `ws_proxy_url` | String | WebSocket代理URL(独立通知配置) | | `ws_url` | String | WebSocket客户端 URL(独立通知配置) | | `ws_token` | String | WebSocket客户端 token(独立通知配置) | -| `action` | String | 发送类型(私聊/群聊)(独立通知配置) | -| `send_id` | String | 发送ID(群号或QQ号,对应发送类型) (独立通知配置) | -| `at_list` | String | @某人列表使用,隔开(QQ号) (独立通知配置) | +| `action` | String | 发送类型(私聊/群聊)(独立通知配置) | +| `send_id` | String | 发送ID(群号或QQ号,对应发送类型) (独立通知配置) | +| `at_list` | String | @某人列表使用,隔开(QQ号) (独立通知配置) | --- @@ -310,6 +382,17 @@ ActivitySwitchNotice/ ## 版本历史 +### 0.0.5 (2026-01-04) + +- **性能优化**:优化滚动到顶部算法,减少页面滚动次数,提升初始化效率 +- **功能增强**:新增条件黑名单过滤机制,支持基于活动状态的动态过滤策略 +- **代码重构**:新增 `parseBlackActivity` 函数,实现黑名单配置的结构化解析 +- **架构改进**:重构黑名单匹配逻辑,引入条件匹配引擎,支持多条件复合判断 +- **数据结构优化**:引入 `blackActivityMap` 配置项,使用 Map 数据结构提升查找性能 +- **逻辑优化**:增强活动过滤算法,集成条件匹配验证机制 +- **初始化流程**:重构配置加载流程,新增 [init]() 函数统一处理配置项初始化 +- **文档完善**:更新配置项文档,补充条件黑名单语法说明 + ### 0.0.4 (2026-01-01) - 新增 独立通知配置功能,支持通过 WebSocket 发送通知 diff --git a/repo/js/ActivitySwitchNotice/main.js b/repo/js/ActivitySwitchNotice/main.js index a3e5ec4ee..839bab2ce 100644 --- a/repo/js/ActivitySwitchNotice/main.js +++ b/repo/js/ActivitySwitchNotice/main.js @@ -1,3 +1,4 @@ +let manifest = {}; async function init() { let utils=[ "ws", @@ -8,7 +9,8 @@ async function init() { for (let util of utils) { eval(file.readTextSync(`utils/${util}.js`)); } - log.info("main 初始化完成"); + manifest = JSON.parse(file.readTextSync("manifest.json")); + log.debug("main 初始化完成"); } // 判断是否在主界面的函数 const isInMainUI = () => { @@ -41,6 +43,7 @@ async function toMainUi() { (async function () { await init(); + log.info(`版本:{version}`,manifest.version) if (settings.toMainUi){ await toMainUi(); } diff --git a/repo/js/ActivitySwitchNotice/manifest.json b/repo/js/ActivitySwitchNotice/manifest.json index 2bc28b1f7..a9fc6ec55 100644 --- a/repo/js/ActivitySwitchNotice/manifest.json +++ b/repo/js/ActivitySwitchNotice/manifest.json @@ -1,6 +1,6 @@ { "name": "活动期限/周本通知器", - "version": "0.0.4", + "version": "0.0.5", "description": "", "settings_ui": "settings.json", "main": "main.js", @@ -13,6 +13,6 @@ "dependencies": [], "http_allowed_urls": [ "https://*", - "http://*", + "http://*" ] } \ No newline at end of file diff --git a/repo/js/ActivitySwitchNotice/settings.json b/repo/js/ActivitySwitchNotice/settings.json index a7cd12d0d..725fa652a 100644 --- a/repo/js/ActivitySwitchNotice/settings.json +++ b/repo/js/ActivitySwitchNotice/settings.json @@ -28,9 +28,9 @@ "label": "白名单活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>" }, { - "name": "blackActivityNameList", + "name": "blackActivity", "type": "input-text", - "label": "黑名单活动名称(使用|分割)<可不填,默认没有不推送的活动>" + "label": "黑名单活动名称(使用|分割)<可不填,默认没有不推送的活动>(新增语法指定条件的黑名单:活动1-条件1,条件2|活动2-条件1)" }, { "name": "notifyHoursThreshold", diff --git a/repo/js/ActivitySwitchNotice/utils/activity.js b/repo/js/ActivitySwitchNotice/utils/activity.js index 69c25bd11..5cb7071cd 100644 --- a/repo/js/ActivitySwitchNotice/utils/activity.js +++ b/repo/js/ActivitySwitchNotice/utils/activity.js @@ -7,21 +7,71 @@ function settingsParseInt(str, defaultValue) { } } -const config = { +/** + * 解析白名单活动文本,将其转换为活动列表 + * @param {string} text - 包含活动信息的文本,多个活动用'|'分隔 + * @param {Array} defaultList - 当text为空时的默认返回列表,默认为空数组 + * @returns {Array} 解析后的活动列表,去除空字符串并过滤空白项 + */ +function parseWhiteActivity(text, defaultList = []) { + return (text ? text.split('|').filter(s => s.trim()) : defaultList) +} + + +/** + * 解析黑名单活动文本 + * @param {string} text - 包含活动信息的文本,使用'|'分隔多个活动,使用'-'分隔活动名称和条件,使用','分隔多个条件 + * @param {Array} excludeList - 需要排除的活动名称列表 + * @returns {Map>|undefined} 解析后的活动映射,键为活动名称,值为条件数组;如果输入文本为空则返回undefined + */ +function parseBlackActivity(text, excludeList = []) { + if (!text) return undefined; + + const result = new Map(); + + // 使用'|'分割文本为多个活动元素 + const splitList = text.split('|'); + for (let element of splitList) { + element = element.trim(); // 清理空白字符 + if (!element) continue; // 跳过空元素 + + // 使用'-'分割活动元素的名称和条件 + const elementList = element.split('-'); + const activityName = elementList[0].trim(); + + if (!activityName) continue; // 跳过没有名称的活动 + + // 检查是否在排除列表中 + if (excludeList.includes(activityName)) { + continue; + } + + // 解析条件:如果有条件部分(length > 1),则分割条件;否则为空数组 + const conditions = elementList.length > 1 + ? Array.from(elementList[1].split(',').map(item => item.trim())) + : []; + log.debug(`parseBlackActivity: ${activityName} - ${conditions}`) + // 将解析后的活动对象添加到结果中 + result.set(activityName, conditions); + } + + return result.size > 0 ? result : undefined; +} + + +let config = { //剩余时间,白名单 启用`和`关系(默认`与`关系) relationship: settings.relationship, - whiteActivityNameList: (settings.whiteActivityNameList ? settings.whiteActivityNameList.split('|').filter(s => s.trim()) : []), + whiteActivityNameList: parseWhiteActivity(settings.whiteActivityNameList), activityKey: (settings.activityKey ? settings.activityKey : 'F5'), toTopCount: settingsParseInt(settings.toTopCount, 10),//滑动到顶最大尝试次数 scrollPageCount: settingsParseInt(settings.scrollPageCount, 4),//滑动次数/页 notifyHoursThreshold: settingsParseInt(settings.notifyHoursThreshold, 8760),//剩余时间阈值(默认 8760小时=365天) // 黑名单活动名称列表,这些活动将被排除在识别和处理之外 // 通过 | 分隔多个活动名称,并过滤掉空白项 + blackActivityMap: parseBlackActivity(settings.blackActivity, parseWhiteActivity(settings.whiteActivityNameList)), // 同时确保黑名单中的活动名称不包含在白名单(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)) - ) : []), + blackActivityNameList: [], } const ocrRegionConfig = { activity: {x: 267, y: 197, width: 226, height: 616},//活动识别区域坐标和尺寸 @@ -61,12 +111,17 @@ const genshinJson = { * * @param {Map} map - 要搜索的Map对象,默认为空Map * @param {string} key - 用于匹配的键名部分字符串 + * @param {boolean} reverseMatch - 开启反向匹配 * @returns {*} 匹配键对应的值,如果未找到匹配项则返回undefined */ -function getMapByKey(map = new Map(), key) { +function getMapByKey(map = new Map(), key, reverseMatch = false) { // 遍历Map的所有键名,查找包含指定key的键 + log.debug('Map=>size:{size}', map.size) for (let keyName of map.keys()) { - if (keyName.includes(key)) { + log.debug('Map=>key:{key},keyName:{keyName},value:{value},ok:{ok}', key, keyName, JSON.stringify(map.get(keyName)), keyName.includes(key)) + if (keyName.includes(key) && !reverseMatch) { + return map.get(keyName) + } else if (key.includes(keyName) && reverseMatch) { return map.get(keyName) } } @@ -131,21 +186,26 @@ async function scrollPage(totalDistance, isUp = false, waitCount = 6, stepDistan await sleep(ms); } + /** - * 根据活动状态 进行页面滚动 - * @param {boolean} isUp - 是否向上滚动,默认为false + * 根据活动页面进行滚动操作 + * @param {boolean} isUp - 滚动方向,true表示向上滚动,false表示向下滚动 + * @param {number} total - 滚动总量 + * @param {number} waitCount - 等待次数 + * @param {number} stepDistance - 每次滚动的步长距离 + * @param {number} scrollPageCount - 滚动页面次数,默认从config中获取 */ -async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30) { +async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30, scrollPageCount = config.scrollPageCount) { // 根据滚动方向设置坐标位置 // 如果是向上滚动,使用顶部坐标;否则使用底部坐标 - let x = isUp ? xyConfig.top.x : xyConfig.bottom.x - let y = isUp ? xyConfig.top.y : xyConfig.bottom.y + let x = isUp ? xyConfig.top.x : xyConfig.bottom.x; // 根据滚动方向获取x坐标 + let y = isUp ? xyConfig.top.y : xyConfig.bottom.y; // 根据滚动方向获取y坐标 // 记录滑动方向 - log.info(`活动页面-${isUp ? '向上' : '向下'}滑动`) + log.info(`活动页面-${isUp ? '向上' : '向下'}滑动`); // 注释:坐标信息已注释掉,避免日志过多 - // log.info(`坐标:${x},${y}`) + // log.info(`坐标:${x},${y}`); // 根据配置的滑动次数执行循环 - for (let i = 0; i < config.scrollPageCount; i++) { + for (let i = 0; i < scrollPageCount; i++) { // 移动到坐标位置 await moveMouseTo(x, y) //80 18次滑动偏移量 46次测试未发现偏移 @@ -160,7 +220,7 @@ async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, st * @throws {Error} 如果超过最大尝试次数仍未检测到稳定顶部,则抛出错误 */ async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) { - let ms = 800 + let ms = 800; // 等待时间,单位毫秒 let topActivityName = null; // 上一次检测到的顶部活动名称 let sameTopCount = 0; // 连续出现相同顶部名称的次数 const requiredSameCount = 1; // 需要连续几次相同才确认到顶(推荐 2~3) @@ -197,7 +257,7 @@ async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) log.warn("顶部OCR未识别到任何活动条目,可能是页面为空或识别失败"); // 再尝试一次向上滚大距离 // await scrollPagesByActivity(true); // true = 向上 - await scrollPagesByActivity(true, 80 * 4, 6, 60); + await scrollPagesByActivity(true, 80 * 4, 6, 60, 1); await sleep(ms); continue; } @@ -225,8 +285,7 @@ async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) // 未达到稳定状态,继续向上滚动一页(可根据实际情况调整滚动距离) // 这里使用更大滚动距离确保能快速回顶 // await scrollPagesByActivity(true); // true = 向上 - // 可选:加大单次滚动量(如果你发现默认一页不够) - await scrollPagesByActivity(true, 80 * 4, 6, 60); + await scrollPagesByActivity(true, 80 * 4, 6, 60, 1); await sleep(ms); // 给页面滚动和渲染留时间 } finally { @@ -260,24 +319,34 @@ function parseRemainingTimeToHours(timeText) { return 0; } - // 提取数字和单位(支持中英文冒号、空格等) - const dayMatch = timeText.match(/(\d+)\s*天/); - const hourMatch = timeText.match(/(\d+)\s*小时/); - let days = 0; let hours = 0; + let minutes = 0; + + // 如果上面的复杂正则有问题,可以使用原来的简化版本 + const dayMatch = timeText.match(/(\d+(?:\.\d+)?)\s*天/); + const hourMatch = timeText.match(/(\d+(?:\.\d+)?)\s*小时/); + const minuteMatch = timeText.match(/(\d+(?:\.\d+)?)\s*分钟/); if (dayMatch) { - days = parseInt(dayMatch[1], 10); + days = parseFloat(dayMatch[1]); } if (hourMatch) { - hours = parseInt(hourMatch[1], 10); + hours = parseFloat(hourMatch[1]); + } + if (minuteMatch) { + minutes = parseFloat(minuteMatch[1]); } - // 天数转小时 + 原有小时 - const totalHours = days * 24 + hours; + // 确保数值非负 + days = Math.max(0, days); + hours = Math.max(0, hours); + minutes = Math.max(0, minutes); - return totalHours; + // 将分钟转换为小时 + const totalHours = days * 24 + hours + minutes / 60; + + return Math.round(totalHours); // 四舍五入到整数 } /** @@ -359,11 +428,20 @@ async function OcrKey(activityName, key = "剩余时间", ocrRegion = ocrRegionC } } +async function init() { + log.debug(`[init-config]-[{config}]`, JSON.stringify(config)); + let blackActivityMap = config.blackActivityMap + config.blackActivityNameList = blackActivityMap ? Array.from(blackActivityMap.keys()) : []; + config.blackActivityNameList.length > 0 && log.debug(`[init]-[{blackActivityNameList}]`, config.blackActivityNameList.join('|')); + log.debug(`[init]-[{ket}]`, 'activity'); + log.debug(`[init-config-end]-[{config}]`, JSON.stringify(config)); +} /** * 活动主函数:扫描所有活动页面,识别剩余时间,最后统一发送通知 */ async function activityMain() { + await init(); const ms = 1000; await sleep(ms); @@ -454,13 +532,42 @@ async function activityMain() { } } + // 检查当前活动名称是否在黑名单中 if (config.blackActivityNameList.length > 0) { - const matched = config.blackActivityNameList.some(keyword => activityName.includes(keyword)); + let matched = config.blackActivityNameList.some(keyword => activityName.includes(keyword)); if (matched) { - continue; // 不关心的活动,跳过不点击 + // 获取黑名单活动的条件配置 + let blackActivityConditions = getMapByKey(config.blackActivityMap, activityName,true); + log.info(`[黑名单条件检测]blackActivityMap:{blackActivityMap},activityName:{activityName},blackActivityConditions:{blackActivityConditions}`, + config.blackActivityMap, activityName, blackActivityConditions); + if (blackActivityConditions && blackActivityConditions.length > 0) { + log.debug('[黑名单条件检测开始]') + matched = false; + // 遍历所有条件,检查是否满足黑名单条件 + for (const blackActivityCondition of blackActivityConditions) { + try { + let condition = await OcrKey(activityName,blackActivityCondition); + if (condition) { + log.info(`满足黑名单条件==>{ac}->{ba}`, activityName, blackActivityCondition); + matched = true; + break; + } + } catch (error) { + log.error(`检查黑名单条件时发生错误: ${error.message}`, error); + // 继续检查下一个条件,不中断整个流程 + continue; + } + } + } + + // 如果匹配到黑名单活动,则跳过不点击 + if (matched) { + continue; // 不关心的活动,跳过不点击 + } } } + // 避免重复点击同一个活动(防止 OCR 误识别或页面抖动) if (activityMap.has(activityName)) { log.info(`活动已记录,跳过重复点击: ${activityName}`); @@ -470,6 +577,11 @@ async function activityMain() { let remainingTimeText = await OcrKey(activityName); if (remainingTimeText) { + if (remainingTimeText.endsWith('小')) { + remainingTimeText += '时' + } else if (remainingTimeText.endsWith('分')) { + remainingTimeText += '钟' + } const totalHours = parseRemainingTimeToHours(remainingTimeText); if (totalHours <= 24 && totalHours > 0) { remainingTimeText += '<即将结束>' @@ -580,7 +692,15 @@ async function activityMain() { let blackText = ""; if (config.blackActivityNameList.length > 0) { - blackText += `|==>已开启黑名单: ${config.blackActivityNameList.join(",")}<==|` + let blackAllText = [] + for (let en of config.blackActivityNameList) { + let configTextList = config.blackActivityMap.get(en) + if (configTextList) { + en += (configTextList.length > 0 ? "-" : "") + configTextList.join(',') + blackAllText.push(en) + } + } + blackText += `==>{已开启黑名单: ${blackAllText.join("|")}}<==` } await noticeUtil.sendNotice(activityMapFilter, `原神活动剩余时间提醒(仅显示 ${titleKey} 的活动)${blackText}`);