Files
bettergi-scripts-list/repo/js/ActivitySwitchNotice/utils/activity.js
云端客 32b950d6a9 [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数组中
- 确保只有有效的文本内容才会被添加到数组中
2025-12-30 18:57:38 +08:00

597 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function settingsParseInt(str, defaultValue) {
try {
return str ? parseInt('' + str) : defaultValue;
} catch (e) {
log.warn(`settingsParseInt error:${e}`)
return defaultValue;
}
}
const config = {
//剩余时间,白名单 启用`和`关系(默认`与`关系)
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天)
// 黑名单活动名称列表,这些活动将被排除在识别和处理之外
// 通过 | 分隔多个活动名称,并过滤掉空白项
// 同时确保黑名单中的活动名称不包含在白名单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},//活动识别区域坐标和尺寸
remainingTime: {x: 497, y: 202, width: 1417, height: 670},//剩余时间识别区域坐标和尺寸
}
const xyConfig = {
top: {x: 344, y: 273},
bottom: {x: 342, y: 791},
}
const DATE_ENUM = Object.freeze({
YEAR: '年',
MON: '月',
WEEK: '周',
DAY: '天',
HOUR: '小时',
// 添加反向映射(可选)
fromValue(value) {
return Object.keys(this).find(key => this[key] === value);
}
});
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
}
/**
* 根据活动名称获取对应的学期转换规则
* @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}
}
/**
* 滚动页面的异步函数
* @param {number} totalDistance - 总滚动距离
* @param {boolean} [isUp=false] - 是否向上滚动默认为false(向下滚动)
* @param {number} [waitCount=6] - 每隔多少步等待一次
* @param {number} [stepDistance=30] - 每步滚动的距离
* @param {number} [delayMs=1] - 等待的延迟时间(毫秒)
*/
async function scrollPage(totalDistance, isUp = false, waitCount = 6, stepDistance = 30, delayMs = 1000) {
let ms = 600
await sleep(ms);
leftButtonDown(); // 按下左键
await sleep(ms);
// 计算总步数
let steps = Math.floor(totalDistance / stepDistance);
// 开始循环滚动
for (let j = 0; j < steps; j++) {
// 计算剩余距离
let remainingDistance = totalDistance - j * stepDistance;
// 确定本次移动距离
let moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance;
// 如果是向上滚动,则移动距离取反
if (isUp) {
//向上活动
moveDistance = -moveDistance
}
// 执行鼠标移动
moveMouseBy(0, -moveDistance);
// 取消注释后会在每一步后等待
// await sleep(delayMs);
// 每隔waitCount步等待一次
if (j % waitCount === 0) {
await sleep(delayMs);
}
}
// 滚动完成后释放左键
await sleep(ms);
leftButtonUp();
await sleep(ms);
}
/**
* 根据活动状态 进行页面滚动
* @param {boolean} isUp - 是否向上滚动默认为false
*/
async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30) {
// 根据滚动方向设置坐标位置
// 如果是向上滚动,使用顶部坐标;否则使用底部坐标
let x = isUp ? xyConfig.top.x : xyConfig.bottom.x
let y = isUp ? xyConfig.top.y : xyConfig.bottom.y
// 记录滑动方向
log.info(`活动页面-${isUp ? '向上' : '向下'}滑动`)
// 注释:坐标信息已注释掉,避免日志过多
// log.info(`坐标:${x},${y}`)
// 根据配置的滑动次数执行循环
for (let i = 0; i < config.scrollPageCount; i++) {
// 移动到坐标位置
await moveMouseTo(x, y)
//80 18次滑动偏移量 46次测试未发现偏移
await scrollPage(total, isUp, waitCount, stepDistance)
}
}
/**
* 滚动到活动页面最顶部(优化版)
* 通过连续检测顶部活动名称相同来确认已到顶,更加健壮
* @param {Object} ocrRegion - OCR识别区域默认为活动列表区域
* @throws {Error} 如果超过最大尝试次数仍未检测到稳定顶部,则抛出错误
*/
async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) {
let ms = 800
let topActivityName = null; // 上一次检测到的顶部活动名称
let sameTopCount = 0; // 连续出现相同顶部名称的次数
const requiredSameCount = 1; // 需要连续几次相同才确认到顶(推荐 2~3
let attemptIndex = 0; // 总尝试次数计数器
const maxAttempts = config.toTopCount; // 可配置默认为15次
log.info("开始滚动到活动页面顶部...");
while (attemptIndex < maxAttempts) {
attemptIndex++;
log.info(`第 {attemptIndex} 次尝试回顶`, attemptIndex);
// 移动鼠标到安全位置,避免干扰截图
await moveMouseTo(0, 20);
// 截图 + OCR 识别活动列表区域
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();
// 如果完全没识别到任何活动,可能是页面异常或已在顶(极少情况)
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();
}
}
}
// 超过最大尝试次数仍未稳定
throw new Error(`回到活动页面顶部失败:尝试 ${attemptIndex} 次后仍未检测到稳定顶部活动`);
}
/**
* 解析原神活动剩余时间字符串,返回总小时数
* 支持格式示例:
* "剩余时间22天14小时" → 54222*24 + 14
* "剩余时间5小时" → 5
* "剩余时间3天" → 723*24 + 0
* "剩余时间1天23小时" → 47
* "剩余10天" → 240也支持部分关键词匹配
*
* @param {string} timeText - OCR识别出的剩余时间文本
* @returns {number} 总剩余小时数(整数,四舍五入向下取整)
* 如果解析失败,返回 0
*/
function parseRemainingTimeToHours(timeText) {
if (!timeText || typeof timeText !== 'string') {
return 0;
}
// 提取数字和单位(支持中英文冒号、空格等)
const dayMatch = timeText.match(/(\d+)\s*天/);
const hourMatch = timeText.match(/(\d+)\s*小时/);
let days = 0;
let hours = 0;
if (dayMatch) {
days = parseInt(dayMatch[1], 10);
}
if (hourMatch) {
hours = parseInt(hourMatch[1], 10);
}
// 天数转小时 + 原有小时
const totalHours = days * 24 + hours;
return totalHours;
}
/**
* 可选:返回格式化字符串,如 "542小时22天14小时"
*/
function formatRemainingTime(timeText) {
if (!timeText || typeof timeText !== 'string') {
return "解析失败";
}
const dayMatch = timeText.match(/(\d+)\s*天/);
const hourMatch = timeText.match(/(\d+)\s*小时/);
const days = dayMatch ? parseInt(dayMatch[1], 10) : 0;
const hours = hourMatch ? parseInt(hourMatch[1], 10) : 0;
const totalHours = days * 24 + hours;
const original = timeText.trim();
return `${totalHours}小时(${days > 0 ? days + '天' : ''}${hours > 0 ? hours + '小时' : ''}`;
}
/**
* 将总小时数转换为周、天和小时的组合表示
* @param {number} totalHours - 需要转换的总小时数
* @returns {string} 返回格式为"X周 Y天 Z小时"的字符串
*/
function convertHoursToWeeksDaysHours(totalHours) {
// 1周 = 168小时 (7 * 24)
const hoursPerWeek = 168;
const hoursPerDay = 24;
// 计算整周数 - 使用Math.floor获取完整的周数
const weeks = Math.floor(totalHours / hoursPerWeek);
// 剩余小时 - 总小时数减去完整周数对应的小时数
let remainingHours = totalHours % hoursPerWeek;
// 从剩余小时中计算天数 - 使用Math.floor获取完整的天数
const days = Math.floor(remainingHours / hoursPerDay);
// 剩余的小时 - 剩余小时数减去完整天数对应的小时数
const hours = remainingHours % hoursPerDay;
// 返回格式化后的字符串,包含周、天和小时
return `${weeks}${days}${hours}小时`;
}
/**
* OCR识别活动剩余时间的函数
* @param {Object} ocrRegion - OCR识别的区域坐标和尺寸
* @param {string} activityName - 活动名称
* @param {string} key - 要识别的关键词,默认为"剩余时间"
* @returns {string|null} 返回识别到的剩余时间文本若未识别到则返回null
*/
async function OcrKey(activityName, key = "剩余时间", ocrRegion = ocrRegionConfig.remainingTime) {
let captureRegion = captureGameRegion(); // 获取游戏区域截图
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(); // 释放截图资源
}
}
/**
* 活动主函数:扫描所有活动页面,识别剩余时间,最后统一发送通知
*/
async function activityMain() {
const ms = 1000;
await sleep(ms);
// 1. 打开活动页面(默认 F5
await keyPress(config.activityKey);
await sleep(ms * 2); // 给活动页面多点加载时间
// 2. 先强制滚动到最顶部(非常重要!)
try {
await scrollPagesByActivityToTop();
await sleep(ms);
} catch (e) {
log.warn("回到顶部失败,但继续尝试执行");
}
// 3. 初始化存储所有活动的 Map
let activityMap = new Map(); // key: 活动名称, value: 剩余时间文本
let previousPageActivities = new Set(); // 新增:记录上一页识别到的所有活动名称(用于重复页判断)
let lastPageBottomName = null; // 上一次扫描到的页面最底部活动名
let sameBottomCount = 0; // 连续出现相同底部活动名的次数
let scannedPages = 0;
const maxPages = 25; // 防止意外死循环的安全上限
let sameBottomCountMax = 1; // 连续相同底部活动名的最大次数
// 4. 主循环:逐页向下扫描
while (scannedPages < maxPages) {
scannedPages++;
log.info(`正在扫描第 {scannedPages} 页`, scannedPages);
// 移动鼠标到安全位置,避免干扰截图
await moveMouseTo(0, 20);
// 获取当前页面活动列表区域截图并 OCR
let captureRegion = null;
try {
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();
// 如果本页完全没有识别到活动,可能是到底了或 OCR 失败
if (resList.length === 0) {
log.info("当前页未识别到任何活动,视为已到页面底部");
break;
}
// ============ 新增:提前判断是否为重复页 ============
const currentPageNames = new Set();
for (let res of resList) {
currentPageNames.add(res.text.trim());
}
// 计算与上一页的重合率
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)}%),已到达底部,停止扫描`);
break;
}
}
// 更新上一页记录(为下一轮做准备)
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; // 不关心的活动,跳过不点击
}
}
if (config.blackActivityNameList.length > 0) {
const matched = config.blackActivityNameList.some(keyword => activityName.includes(keyword));
if (matched) {
continue; // 不关心的活动,跳过不点击
}
}
// 避免重复点击同一个活动(防止 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++;
}
await sleep(ms);
}
// 更新本页最下面的活动名
currentPageBottomName = activityName;
}
// 备用判断:本页一个新活动都没加,也认为到底(双保险)
if (newActivityCountThisPage === 0 && scannedPages > 1) {
log.info("本页无新活动添加,确认已到底");
break;
}
// 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);
} finally {
if (captureRegion) {
captureRegion.dispose();
}
}
}
let activityMapFilter = new Map();
Array.from(activityMap.entries())
.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);
// 构建通知标题,根据配置显示剩余时间阈值和白名单活动信息
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("不存在符合条件的活动,未发送通知");
}
}
this.activityUtil = {
// config,
activityMain,
// OcrRemainingTime,
}