[0.0.3版本迭代]--活动期限/周本通知器 (#2591)

* 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数组中
- 确保只有有效的文本内容才会被添加到数组中
This commit is contained in:
云端客
2025-12-30 18:57:38 +08:00
committed by GitHub
parent ad6dd12602
commit 32b950d6a9
6 changed files with 313 additions and 220 deletions

View File

@@ -36,7 +36,8 @@
| 设置项 | 说明 | 默认值 | 开放 |
|:---------------------------|:-----------------------------------------------|:------------|:--:|
| `toMainUi` | 执行前是否自动返回游戏主界面 | true | v |
| `activityNameList` | 监控的特定活动名称(用\|分隔) | 空(监控所有活动) | v |
| `relationship` | 剩余时间与白名单启用`和`关系(默认`或`关系) | false | v |
| `whiteActivityNameList` | 白名单活动名称(用\|分隔) | 空(监控所有活动) | v |
| `blackActivityNameList` | 黑名单活动名称(用\|分隔) | 空(无黑名单活动) | v |
| `notifyHoursThreshold` | 通知时间阈值(小时) | 8760365天 | 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)
- 新增 征讨领域周次数提醒功能

View File

@@ -1,6 +1,6 @@
{
"name": "活动期限/周本通知器",
"version": "0.0.2",
"version": "0.0.3",
"description": "",
"settings_ui": "settings.json",
"main": "main.js",

View File

@@ -6,9 +6,15 @@
"default": true
},
{
"name": "activityNameList",
"name": "relationship",
"type": "checkbox",
"label": "剩余时间,白名单 启用`和`关系(默认`或`关系)",
"default": false
},
{
"name": "whiteActivityNameList",
"type": "input-text",
"label": "活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>"
"label": "白名单活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>"
},
{
"name": "blackActivityNameList",

View File

@@ -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<void>} 无返回值,当到达顶部时函数执行结束
* @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("不存在符合条件的活动,未发送通知");
}
}

View File

@@ -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
// 等待一段时间

View File

@@ -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 // 直接返回,不执行后续操作
}