mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-04-04 06:46:19 +08:00
活动期限通知器 (#2535)
* feat(js): 初始化活动期限通知器插件 - 创建主执行文件 main.js - 添加插件配置文件 manifest.json - 编写基础设置界面 settings.json - 建立项目说明文档 README.md - 实现异步主函数结构 - 配置插件元数据和依赖项 - 定义用户可配置选项 - 设置默认启用状态和快捷键 - 添加作者信息和联系方式 - 描述核心功能和使用注意事项 * feat(activity): 添加活动切换通知功能 - 新增活动配置读取逻辑,支持指定活动名称列表和活动键 - 实现活动主函数,根据配置决定通知所有或指定活动 - 添加通知工具模块,支持异步发送包含标题和内容的通知 - 在主文件中引入活动和通知工具模块,完善执行流程 * feat(activity): 新增OCR点击活动功能 - 实现OcrClickActivity函数,支持OCR识别并点击活动 - 添加活动名称匹配与切换计数逻辑 - 记录活动坐标及剩余时间信息 - 更新activityMain函数以配合新活动切换流程 - 优化活动页面打开与关闭的键盘操作逻辑 * feat(activity): 增加活动OCR识别与剩余时间获取功能 - 新增ocrRegionConfig配置对象,定义活动及剩余时间识别区域 - 修改OcrClickActivity函数,支持默认OCR区域配置和活动识别逻辑优化 - 增加activityOk状态标识,用于判断是否完成所有活动识别 - 实现OcrRemainingTime函数,用于识别并返回指定活动的剩余时间 - 重构activityMain主流程,支持循环识别多个活动及其剩余时间 - 集成通知模块,发送活动名称和对应剩余时间的通知消息 - 引入外部脚本执行机制,通过eval动态加载notice.js配置文件 * chore(activity): 添加待办注释以优化活动切换逻辑 - 在活动切换循环前添加 '//todo:拉到顶部' 注释 - 为后续优化活动切换时的滚动行为做准备 * feat(activity): 增加活动切换尝试次数限制 - 添加 index 和 maxIndex 变量控制循环次数 - 设置最大尝试次数为10次 - 超出最大尝试次数时记录警告日志并退出循环 - 避免无限循环导致脚本卡死 * chore(activity): 添加待办注释和空行优化代码结构 - 在循环开始前添加了两个新的待办注释 - 在条件判断后添加了滑动一页的待办注释 - 为代码逻辑段落间增加了空行以提高可读性 * chore(activity): 更新活动切换注释 - 将注释中的“滑动一页”修改为“向下滑动一页”以提高可读性 * feat(activity): 实现活动自动化处理主函数 - 添加活动主函数 activityMain 用于自动化处理活动流程 - 集成打开活动页面、OCR识别与点击功能 - 引入活动Map记录已识别活动避免重复处理 - 实现循环处理机制并支持指定活动列表过滤 - 增加页面滑动到底判断逻辑防止无限循环 - 添加最大尝试次数限制及对应日志警告 - 完善注释说明提升代码可读性与维护性 * fix(ActivitySwitchNotice): 调整OCR识别区域坐标配置 - 更新活动识别区域坐标为(197, 220, 292, 701) - 更新剩余时间识别区域坐标为(497, 202, 1417, 670) - 优化识别精度以提高切换检测准确性 * refactor(activity): 重构活动切换通知逻辑 - 移除冗余的notice.js文件引用 - 引入activity.js工具文件 - 更新主函数调用activityUtil.activityMain方法 - 确保异步执行上下文正确处理 * feat(activity): 添加页面滚动功能 - 新增 scrollPage 异步函数用于控制页面滚动 - 支持设置滚动方向、距离和速度参数 - 实现鼠标按键控制和步进式滚动逻辑 - 添加详细的函数文档注释 - 集成延时控制和步长配置选项 * feat(activity): 添加活动切换通知功能 - 新增xyConfig配置对象定义top和bottom坐标 - 在activity.js中引入notice.js脚本 - 优化代码格式调整maxIndex变量声明空格 - 修复条件判断语句中的语法错误 - 完善活动切换逻辑注释说明 * feat(activity): 新增活动页面滚动功能 - 添加genshinJson配置对象定义画布尺寸 - 实现scrollPagesByActivity函数支持页面滚动 - 支持向上或向下滚动操作 - 集成坐标移动与页面滚动逻辑 - 设置默认滚动参数适配画布高度 - 保留原有OCR点击活动功能 * refactor(activity): 优化活动点击处理函数参数和映射逻辑 * refactor(activity): 优化活动切换通知的滚动逻辑 - 移除eval执行外部脚本的方式,提高安全性 - 更新scrollPagesByActivity函数实现更稳定的滚动操作 - 增加循环次数与调整滚动参数以提升准确性 - 导出新增的工具方法和配置对象供其他模块使用 * feat(activity): 优化活动切换与滚动逻辑 - 修改鼠标移动注释,明确移动到指定坐标位置 - 为OcrClickActivity函数新增defaultActivityCount参数,支持传入初始活动计数 - 在活动识别主循环中提前声明switchToActivityCount变量,便于复用 - 调整OcrClickActivity调用方式,传递已有的switchToActivityCount值 * feat(activity): 新增滑动到顶功能并优化滑动逻辑 - 新增 scrollPagesByActivityToTop 方法实现自动滚动至顶部 - 增加 toTopCount 和 scrollPageCount 配置项用于控制滑动行为 - 修改 scrollPagesByActivity 方法支持可配置的滑动次数 - 调整 OCR 活动点击函数参数格式以提高可读性 * feat(activity): 实现活动页面滚动功能 - 添加滚动到顶部功能,调用scrollPagesByActivityToTop方法 - 实现向下滑动一页功能,调用scrollPagesByActivity方法 - 移除待办注释,完成页面滚动逻辑 - 整合滚动功能到主循环中,优化用户体验 * feat(ActivitySwitchNotice): 增加返回主界面逻辑并优化设置提示 - 新增 isInMainUI 函数用于判断当前是否处于主界面 - 实现 toMainUi 异步函数确保脚本运行前回到主界面 - 更新设置项标签,明确活动名称可选填且默认推送所有有剩余时间的活动 - 调整代码结构以支持新增功能模块 - 修复部分语法问题以提高代码健壮性 * 图 * feat(ActivitySwitchNotice): 添加控制是否先返回主界面的设置选项 - 修改设置项名称从 enable 为 toMainUi - 根据设置值控制是否执行返回主界面逻辑 - 更新设置标签描述信息 - 默认启用该功能以保持向后兼容性 * chore(activity): 调整活动工具函数导出并优化主入口执行逻辑 - 注释掉 activityUtil 中的 OcrRemainingTime 导出项 - 在 main.js 中添加异步立即执行函数以支持 toMainUi 切换逻辑 - 为主入口逻辑增加 settings.toMainUi 条件判断 * feat(ActivitySwitchNotice): 添加默认按键设置选项 - 在设置中为活动页面按键添加默认值"F5" - 更新配置以支持自定义按键绑定 - 优化用户体验,减少手动输入需求 * feat(ActivitySwitchNotice): 添加活动配置并触发按键事件 - 在 activityUtil 中暴露 config 配置对象 - 在主流程中添加按键事件调用逻辑 - 使用 activityKey 配置触发键盘操作 * fix(ActivitySwitchNotice): 优化活动切换逻辑并移除冗余配置 - 将活动快捷键模拟从 keyDown/keyUp 改为 keyPress - 移除多余的按键释放操作 - 注释掉未使用的 config 导出 - 移除主函数中重复的按键触发逻辑 * refactor(activity): 替换等待函数提升代码可读性 - 将 wait() 函数调用替换为 sleep(ms) - 统一使用 sleep 函数处理异步等待逻辑 - 提高代码一致性和维护性 * fix(activity): 修复回到顶部逻辑判断条件 - 修改循环跳出条件判断逻辑,避免无限循环 - 确保超过最大尝试次数时能正确抛出错误 - 提高页面滚动到顶部功能的稳定性 * fix(activity): 更新OCR识别区域配置 - 将OCR识别区域从ocrRegion变量更改为config.activity配置 - 确保OCR识别使用正确的活动区域参数 - 保持原有的注释和逻辑结构不变 * feat(activity): 支持自定义OCR识别区域配置 - 修改scrollPagesByActivityTop函数签名,新增ocrRegion参数支持传入自定义OCR区域配置 - 默认使用ocrRegionConfig.activity作为OCR识别区域配置 - 更新OCR对象创建逻辑,使用传入的ocrRegion参数替代原有的config.activity配置 - 保持原有功能不变,增强函数灵活性和可配置性 * fix(activity): 调整活动识别区域坐标和尺寸 - 修改activity识别区域的x、y坐标及宽高参数 - 更新width从292到226,height从701到616 - 调整x坐标从197到267,y坐标从220到197 * fix(activity): 修复活动地图更新逻辑 - 修正 activityMap 初始化赋值错误 - 更新活动地图遍历方式,使用 keys() 方法 - 修复活动地图新增逻辑,避免重复设置相同键值 - 确保只添加新发现的活动到 activityMap 中 * fix(ActivitySwitchNotice): 修复通知文本中键值对顺序错误 - 调整forEach回调函数参数顺序,确保键值对正确显示 - 保持代码逻辑一致性,避免数据展示混乱 * feat(activity): 增加活动切换次数记录功能 - 在OcrClickActivity函数中新增lastName参数用于记录上一个活动名称 - 新增resObject对象统一管理活动切换状态和坐标信息 - 优化活动识别逻辑,避免重复识别相同活动 - 更新函数调用方式以传递LastActivityName参数 - 修复活动切换计数可能不准确的问题 - 完善活动地图更新机制,确保数据一致性 * feat(activity): 添加鼠标移动操作以优化活动切换逻辑 - 在回到顶部逻辑前添加鼠标移动至坐标(0,20)的操作 - 在OCR点击活动逻辑前添加鼠标移动至坐标(0,20)的操作 - 提升活动切换准确性与稳定性 * fix(ActivitySwitchNotice): 调整通知文本格式 - 修改键值对分隔符为单个空格 - 移除键值之间的冒号符号 - 保持每行末尾换行符一致 * fix(ActivitySwitchNotice): 修复活动切换工具中的OCR识别逻辑 - 统一鼠标移动函数调用的代码风格,增加参数间的空格 - 在成功返回顶部时添加日志信息输出 - 增强OCR识别结果判断条件,避免访问空数组导致异常 * fix(ActivitySwitchNotice): 优化活动切换逻辑和OCR识别流程 - 修改OcrClickActivity函数参数默认值,将lastName默认值设为null - 增强活动识别结束条件判断,避免无限循环 - 添加活动识别过程中的日志记录以便调试 - 引入双变量记录上一个活动名称以提高准确性 - 重构主循环逻辑,确保能正确识别所有活动 - 修复在特定条件下无法退出循环的问题 - 完善活动切换次数统计和活动映射更新机制 * feat(activity): 优化活动页面滚动与OCR识别逻辑 - 新增 scrollPagesByActivityToTop 函数,通过连续检测顶部活动名称相同来确认已到顶 - 修改 scrollPagesByActivity 支持自定义滚动参数(total, waitCount, stepDistance) - 重构 activityMain 函数,改为逐页扫描活动并统一发送通知 - 调整 OCR 日志级别从 info 改为 debug - 注释掉旧版 OcrClickActivity 和 activityMain 实现 - 在 main.js 中增加一次 toMainUi 调用以确保界面状态稳定 * refactor(activity): 移除废弃的活动处理函数并优化日志输出 - 删除了已注释的 OcrClickActivity 函数实现 - 移除了已注释的 activityMain 主函数代码 - 优化了活动剩余时间识别的日志格式 - 清理了无用的活动处理逻辑和冗余注释 - 简化了活动识别与点击的核心流程 - 提升了代码可读性和维护性 * fix(notice): 修复通知文本格式化问题 - 修复了模板字符串中变量未正确插入的问题 - 确保键值对在通知中正确显示 - 保持换行符一致以维持原有格式 * fix(ActivitySwitchNotice): 修复通知文本格式化问题 - 将字符串拼接方式从双引号改为模板字符串 - 修复了键值对显示异常的问题 - 确保通知文本正确换行显示 * fix(notice): 移除多余的通知文本包装 - 删除了发送通知时多余的反引号包裹 - 确保通知文本格式正确显示 - 避免了因格式错误导致的通知发送失败问题 * feat(activity): 新增活动剩余时间解析与排序通知功能 - 添加解析原神活动剩余时间字符串为总小时数的函数 - 支持多种时间格式如"22天14小时"、"5小时"等 - 实现按剩余小时数升序排列活动通知内容 - 更新活动时间存储结构以包含原始文本和计算后的小时数 - 优化通知消息展示格式,突出剩余时间信息 * fix(activity): 修复活动时间记录日志格式 - 在日志信息中明确添加“小时”单位以提高可读性 - 保持原有变量替换逻辑不变 - 确保日志输出的一致性和准确性 * feat(activity): 增强活动页面底部检测逻辑 - 新增基于活动名称集合的重复页判断机制 - 计算当前页与上一页活动名称重合率,防止无效滚动 - 设置重合率阈值为70%,超过则判定已到底部 - 每页新增活动计数器,若为零且非首页则停止扫描 - 保留原有底部判断方式,形成双重保险机制 - 优化日志输出,增强调试与运行状态可视化 * feat(activity): 新增活动去重逻辑 - 引入previousPageActivities集合用于记录已识别活动 - 防止同一页活动被重复处理 - 提升活动切换提醒准确性 * feat(activity): 增强活动扫描与通知功能 - 新增 settingsParseInt 函数用于安全解析配置数值 - 添加 notifyHoursThreshold 配置项,支持自定义通知时间阈值 - 优化滚动逻辑中的参数格式和日志输出 - 增加活动即将结束标识(剩余时间≤24小时) - 过滤掉超出时间阈值的活动,仅通知符合条件的活动 - 更新通知文案,显示时间筛选条件 - 修复多处代码格式和空格问题,提升可读性 * docs(ActivitySwitchNotice): 更新README文档内容 - 补充项目概述与功能特性说明 - 添加用户使用指南与快速开始步骤 - 详细描述配置选项与高级设置 - 增加文件结构与核心模块介绍 - 完善注意事项与工作原理说明 - 提供输出示例与使用环境要求 - 整理文档结构,提升可读性与实用性 * docs(ActivitySwitchNotice): 更新 README 配置说明 - 修正 settings.json 文件链接格式 - 统一文档中文件引用的展示方式 - 提高配置说明的可读性 * fix(ActivitySwitchNotice): 修复设置项默认值语法错误 - 修正 settings.json 中 notifyHoursThreshold 字段的默认值语法 - 添加缺失的冒号以符合 JSON 格式要求 * fix(ActivitySwitchNotice): 修复活动通知筛选逻辑并优化排序 - 调整活动筛选条件,确保仅包含剩余时间小于阈值的活动 - 对筛选后的活动按剩余时间进行升序排序 - 更新通知发送逻辑以使用排序后的活动列表 - 修正日志记录中的活动数量显示问题 - 移除无用的代码注释和空行 * feat(activity): 优化活动通知逻辑以提高性能 - 使用 Map 过滤活动数据而不是数组排序 - 减少不必要的数据转换操作 - 提升活动扫描和通知发送的效率 - 保持原有通知阈值和日志功能不变 * fix(activity): 修复活动名称列表分割方法 - 将 splice 方法更正为 split 方法以正确分割活动名称 - 更新 README 中的默认通知阈值描述
This commit is contained in:
175
repo/js/ActivitySwitchNotice/README.md
Normal file
175
repo/js/ActivitySwitchNotice/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 活动期限通知器
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个用于《原神》游戏的自动化脚本工具,主要功能是自动检测游戏内活动的剩余时间,并在活动即将结束时发送通知提醒玩家。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 自动返回游戏主界面并打开活动页面
|
||||
- ✅ OCR识别活动列表和剩余时间
|
||||
- ✅ 自动滚动浏览所有活动页面
|
||||
- ✅ 智能解析剩余时间(支持"22天14小时"等格式)
|
||||
- ✅ 可配置的通知阈值(默认8760小时内结束的活动)
|
||||
- ✅ 支持指定特定活动进行监控
|
||||
- ✅ 防重复检测机制
|
||||
- ✅ 异常处理和错误恢复
|
||||
|
||||
---
|
||||
|
||||
## 用户使用指南
|
||||
|
||||
### 快速开始
|
||||
|
||||
#### 1. 安装与配置
|
||||
- 确保游戏分辨率为 **1920×1080**(推荐分辨率)
|
||||
- 将脚本导入到 BetterGI 脚本管理器中
|
||||
- 在脚本设置界面进行个性化配置
|
||||
|
||||
#### 2. 基础设置
|
||||
在 [settings.json]() 中可以配置以下参数:
|
||||
|
||||
| 设置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `toMainUi` | 执行前是否自动返回游戏主界面 | true |
|
||||
| `activityNameList` | 监控的特定活动名称(用\|分隔) | 空(监控所有活动) |
|
||||
| `notifyHoursThreshold` | 通知时间阈值(小时) | 8760(365天) |
|
||||
| `activityKey` | 打开活动页面的快捷键 | F5 |
|
||||
|
||||
### 使用流程
|
||||
|
||||
#### 自动模式(推荐)
|
||||
1. 启动脚本后,程序会自动:
|
||||
- 检测当前是否在游戏主界面
|
||||
- 如未在主界面,自动返回主界面
|
||||
- 按设定的快捷键打开活动页面
|
||||
- 开始扫描所有活动
|
||||
|
||||
#### 手动模式
|
||||
1. 关闭 `toMainUi` 选项
|
||||
2. 确保游戏处于主界面状态
|
||||
3. 启动脚本开始扫描
|
||||
|
||||
### 功能详解
|
||||
|
||||
#### 活动筛选
|
||||
- **全部活动监控**:`activityNameList` 保持空值,监控所有有剩余时间的活动
|
||||
- **指定活动监控**:填写活动关键词,如 `海灯节\|盛典`,只监控包含这些关键词的活动
|
||||
|
||||
#### 时间通知机制
|
||||
- 默认监控所有活动(`notifyHoursThreshold`=8760小时)
|
||||
- 可设置阈值,如设置为24,则只通知剩余时间≤24小时的活动
|
||||
- 即将结束的活动会在通知中标记 `<即将结束>`
|
||||
|
||||
#### 智能防重复
|
||||
- 自动识别已扫描过的活动页面
|
||||
- 防止因页面滚动不准确造成的重复识别
|
||||
- 自动判断是否已滚动到页面底部
|
||||
|
||||
### 注意事项
|
||||
|
||||
#### 使用环境要求
|
||||
- ✅ 游戏分辨率为 1920×1080(最佳兼容性)
|
||||
- ✅ 游戏处于前台运行状态
|
||||
- ✅ 活动页面可通过设置的快捷键正常打开
|
||||
|
||||
#### 运行期间注意事项
|
||||
- 🚫 运行期间请勿手动操作鼠标
|
||||
- 🚫 避免切换窗口或最小化游戏
|
||||
- ⚠️ 如遇异常可重新启动脚本
|
||||
|
||||
### 高级配置
|
||||
|
||||
#### 自定义快捷键
|
||||
如活动页面不是F5打开,可在 `activityKey` 中修改为对应按键。
|
||||
|
||||
#### 时间阈值设置
|
||||
根据个人需求设置 `notifyHoursThreshold`:
|
||||
- 24:只关注24小时内结束的活动
|
||||
- 168:关注一周内结束的活动
|
||||
- 720:关注一个月内结束的活动
|
||||
|
||||
### 输出示例
|
||||
|
||||
通知消息格式如下:
|
||||
```
|
||||
原神活动剩余时间提醒:
|
||||
> 海灯节庆典 剩余时间:3天14小时<还剩 86 小时>
|
||||
> 风花节活动 剩余时间:1天5小时<还剩 29 小时><即将结束>
|
||||
```
|
||||
|
||||
**`以上为用户使用指南全部内容`**
|
||||
---
|
||||
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
ActivitySwitchNotice/
|
||||
├── utils/
|
||||
│ ├── activity.js # 核心活动处理逻辑
|
||||
│ └── notice.js # 通知发送功能
|
||||
├── main.js # 主入口文件
|
||||
├── manifest.json # 插件配置文件
|
||||
├── settings.json # 用户设置界面定义
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
|
||||
## 核心模块
|
||||
|
||||
### [activity.js]() - 活动处理核心
|
||||
|
||||
主要包含以下功能函数:
|
||||
|
||||
- `scrollPage()` - 页面滚动基础函数
|
||||
- `scrollPagesByActivity()` - 按页滚动活动列表
|
||||
- `scrollPagesByActivityToTop()` - 滚动到活动列表顶部
|
||||
- `parseRemainingTimeToHours()` - 解析剩余时间文本为小时数
|
||||
- `OcrRemainingTime()` - OCR识别剩余时间
|
||||
- `activityMain()` - 主流程控制函数
|
||||
|
||||
### `notice.js` - 通知模块
|
||||
|
||||
- `sendNotice()` - 发送活动提醒通知,按剩余时间排序
|
||||
|
||||
### [main.js]() - 主程序入口
|
||||
|
||||
- 检测是否在主界面
|
||||
- 返回主界面功能
|
||||
- 执行主流程
|
||||
|
||||
## 配置选项
|
||||
|
||||
在 [settings.json]() 中可配置以下参数:
|
||||
|
||||
| 配置项 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| `toMainUi` | Boolean | 是否先返回主界面再执行 |
|
||||
| `activityNameList` | String | 指定活动名称(用\|分隔) |
|
||||
| `notifyHoursThreshold` | Number | 通知阈值(小时) |
|
||||
| `activityKey` | String | 打开活动页面的快捷键 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. 自动返回游戏主界面
|
||||
2. 按配置快捷键打开活动页面
|
||||
3. 滚动到活动列表顶部
|
||||
4. 逐页扫描所有活动
|
||||
5. OCR识别每个活动的剩余时间
|
||||
6. 解析时间为小时数并过滤
|
||||
7. 发送符合条件的活动提醒
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 请确保游戏分辨率为1920×1080以获得最佳效果
|
||||
- 脚本运行期间请勿操作鼠标
|
||||
- 某些特殊活动可能无法正确识别剩余时间
|
||||
- 建议在游戏空闲时运行此脚本
|
||||
|
||||
---
|
||||
|
||||
## 其它
|
||||
|
||||
作者:云端客
|
||||
脚本反馈邮箱:doutianmianxia@qq.com
|
||||
BIN
repo/js/ActivitySwitchNotice/assets/paimon_menu.png
Normal file
BIN
repo/js/ActivitySwitchNotice/assets/paimon_menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
46
repo/js/ActivitySwitchNotice/main.js
Normal file
46
repo/js/ActivitySwitchNotice/main.js
Normal file
@@ -0,0 +1,46 @@
|
||||
eval(file.readTextSync(`utils/activity.js`));
|
||||
eval(file.readTextSync(`utils/notice.js`));
|
||||
|
||||
// 判断是否在主界面的函数
|
||||
const isInMainUI = () => {
|
||||
let captureRegion = captureGameRegion();
|
||||
let res = captureRegion.Find(RecognitionObject.TemplateMatch(
|
||||
file.ReadImageMatSync("assets/paimon_menu.png"),
|
||||
0,
|
||||
0,
|
||||
640,
|
||||
216
|
||||
));
|
||||
captureRegion.dispose();
|
||||
return !res.isEmpty();
|
||||
};
|
||||
|
||||
async function toMainUi() {
|
||||
let ms = 300
|
||||
let index = 1
|
||||
await sleep(ms);
|
||||
while (!isInMainUI()) {
|
||||
await sleep(ms);
|
||||
await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面
|
||||
await sleep(ms);
|
||||
if (index > 3) {
|
||||
throw new Error(`多次尝试返回主界面失败`);
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
(async function () {
|
||||
if (settings.toMainUi){
|
||||
await toMainUi();
|
||||
}
|
||||
await main();
|
||||
await toMainUi();
|
||||
})();
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function main() {
|
||||
await activityUtil.activityMain()
|
||||
}
|
||||
14
repo/js/ActivitySwitchNotice/manifest.json
Normal file
14
repo/js/ActivitySwitchNotice/manifest.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "活动期限通知器",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"settings_ui": "settings.json",
|
||||
"main": "main.js",
|
||||
"authors": [
|
||||
{
|
||||
"name": "云端客",
|
||||
"links": "https://github.com/Kirito520Asuna"
|
||||
}
|
||||
],
|
||||
"dependencies": []
|
||||
}
|
||||
25
repo/js/ActivitySwitchNotice/settings.json
Normal file
25
repo/js/ActivitySwitchNotice/settings.json
Normal file
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"name": "toMainUi",
|
||||
"type": "checkbox",
|
||||
"label": "启用先返回主界面后执行切换",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "activityNameList",
|
||||
"type": "input-text",
|
||||
"label": "活动名称(使用|分割)<可不填 默认推送所有有剩余时间的活动>"
|
||||
},
|
||||
{
|
||||
"name": "notifyHoursThreshold",
|
||||
"type": "input-text",
|
||||
"label": "通知剩余时间阈值<单位:小时>(默认 8760小时=365天)",
|
||||
"default": "8760"
|
||||
},
|
||||
{
|
||||
"name": "activityKey",
|
||||
"type": "input-text",
|
||||
"label": "打开活动页面按键(不填,默认:F5)",
|
||||
"default": "F5"
|
||||
}
|
||||
]
|
||||
446
repo/js/ActivitySwitchNotice/utils/activity.js
Normal file
446
repo/js/ActivitySwitchNotice/utils/activity.js
Normal file
@@ -0,0 +1,446 @@
|
||||
function settingsParseInt(str, defaultValue) {
|
||||
try {
|
||||
return str ? parseInt('' + str) : defaultValue;
|
||||
} catch (e) {
|
||||
log.warn(`settingsParseInt error:${e}`)
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
activityNameList: (settings.activityNameList ? settings.activityNameList.split('|') : []),
|
||||
activityKey: (settings.activityKey ? settings.activityKey : 'F5'),
|
||||
toTopCount: settingsParseInt(settings.toTopCount,10),//滑动到顶最大尝试次数
|
||||
scrollPageCount: settingsParseInt(settings.scrollPageCount,4),//滑动次数/页
|
||||
notifyHoursThreshold: settingsParseInt(settings.notifyHoursThreshold, 8760),//剩余时间阈值(默认 8760小时=365天)
|
||||
}
|
||||
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 genshinJson = {
|
||||
width: 1920,//genshin.width,
|
||||
height: 1080,//genshin.height,
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动页面的异步函数
|
||||
* @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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过滚动页面直到到达顶部位置
|
||||
* 该函数会持续滚动页面,直到检测到页面顶部的标识不再变化为止
|
||||
* @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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到活动页面最顶部(优化版)
|
||||
* 通过连续检测顶部活动名称相同来确认已到顶,更加健壮
|
||||
* @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 = 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); // 给页面滚动和渲染留时间
|
||||
}
|
||||
|
||||
// 超过最大尝试次数仍未稳定
|
||||
throw new Error(`回到活动页面顶部失败:尝试 ${attemptIndex} 次后仍未检测到稳定顶部活动`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析原神活动剩余时间字符串,返回总小时数
|
||||
* 支持格式示例:
|
||||
* "剩余时间:22天14小时" → 542(22*24 + 14)
|
||||
* "剩余时间:5小时" → 5
|
||||
* "剩余时间:3天" → 72(3*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 + '小时' : ''})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* OCR识别活动剩余时间的函数
|
||||
* @param {Object} ocrRegion - OCR识别的区域坐标和尺寸
|
||||
* @param {string} activityName - 活动名称
|
||||
* @param {string} key - 要识别的关键词,默认为"剩余时间"
|
||||
* @returns {string|null} 返回识别到的剩余时间文本,若未识别到则返回null
|
||||
*/
|
||||
async function OcrRemainingTime(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 // 返回识别到的文本
|
||||
}
|
||||
}
|
||||
// 没有识别到剩余时间
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活动主函数:扫描所有活动页面,识别剩余时间,最后统一发送通知
|
||||
*/
|
||||
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 = 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.activityNameList.length > 0) {
|
||||
const matched = config.activityNameList.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 OcrRemainingTime(activityName);
|
||||
if (remainingTimeText) {
|
||||
const totalHours = parseRemainingTimeToHours(remainingTimeText);
|
||||
if (totalHours <= 24 && totalHours > 0) {
|
||||
remainingTimeText += '<即将结束>'
|
||||
}
|
||||
activityMap.set(activityName, {
|
||||
text: remainingTimeText,
|
||||
hours: totalHours
|
||||
});
|
||||
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);
|
||||
}
|
||||
let activityMapFilter = new Map();
|
||||
Array.from(activityMap.entries())
|
||||
.filter(([name, info]) => info.hours <= config.notifyHoursThreshold)
|
||||
.forEach(([name, info]) => activityMapFilter.set(name, info));
|
||||
// 7. 全部扫描完毕,统一发送通知(只发一次!)
|
||||
if (activityMapFilter.size > 0) {
|
||||
log.info(`扫描完成,共记录 {activityMap.size} 个活动,即将发送通知`, activityMapFilter.size);
|
||||
await noticeUtil.sendNotice(activityMapFilter, `原神活动剩余时间提醒(仅显示剩余 ≤ ${config.notifyHoursThreshold} 小时的活动):`);
|
||||
} else {
|
||||
log.warn("未识别到任何活动,未发送通知");
|
||||
}
|
||||
}
|
||||
|
||||
this.activityUtil = {
|
||||
// config,
|
||||
activityMain,
|
||||
// OcrRemainingTime,
|
||||
}
|
||||
27
repo/js/ActivitySwitchNotice/utils/notice.js
Normal file
27
repo/js/ActivitySwitchNotice/utils/notice.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 发送通知的异步函数
|
||||
* @param {Map} map - 包含通知内容键值对的Map对象
|
||||
* @param {string} title - 通知的标题
|
||||
* @param {boolean} noNotice - 是否不发送通知的标志
|
||||
*/
|
||||
async function sendNotice(map, title, noNotice) {
|
||||
// 如果设置了不发送通知且map为空,则记录日志并返回
|
||||
if (noNotice && !map) {
|
||||
log.info(`无通知内容`)
|
||||
return
|
||||
}
|
||||
// 按剩余小时升序排序(即将结束的在前)
|
||||
const sortedEntries = Array.from(map.entries())
|
||||
.sort((a, b) => a[1].hours - b[1].hours);
|
||||
|
||||
let noticeText = title ? title + "\n" : "\n"
|
||||
for (const [name, info] of sortedEntries) {
|
||||
noticeText += `> ${name} ${info.text}<还剩 ${info.hours} 小时>\n`;
|
||||
}
|
||||
// 发送通知
|
||||
notification.send(noticeText)
|
||||
}
|
||||
|
||||
this.noticeUtil = {
|
||||
sendNotice,
|
||||
}
|
||||
Reference in New Issue
Block a user