mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-05-20 22:25:50 +08:00
【0.0.5】活动期限/周本通知器 (#2618)
* 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): 更新活动过滤器流程图文档 - 修正了活动过滤器流程图中的条件判断逻辑描述 - 更新了黑名单匹配条件的处理流程说明
This commit is contained in:
@@ -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 发送通知
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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://*"
|
||||
]
|
||||
}
|
||||
@@ -28,9 +28,9 @@
|
||||
"label": "白名单活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>"
|
||||
},
|
||||
{
|
||||
"name": "blackActivityNameList",
|
||||
"name": "blackActivity",
|
||||
"type": "input-text",
|
||||
"label": "黑名单活动名称(使用|分割)<可不填,默认没有不推送的活动>"
|
||||
"label": "黑名单活动名称(使用|分割)<可不填,默认没有不推送的活动>(新增语法指定条件的黑名单:活动1-条件1,条件2|活动2-条件1)"
|
||||
},
|
||||
{
|
||||
"name": "notifyHoursThreshold",
|
||||
|
||||
@@ -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<string>} excludeList - 需要排除的活动名称列表
|
||||
* @returns {Map<string, Array<string>>|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}`);
|
||||
|
||||
Reference in New Issue
Block a user