修改记录保存逻辑 (#2517)

* 修改记录保存逻辑

* Update main.js
This commit is contained in:
skyflag2022
2025-12-17 20:41:23 +08:00
committed by GitHub
parent fa30b68646
commit 4143fb3ea5
3 changed files with 311 additions and 197 deletions

View File

@@ -8,7 +8,11 @@
原理是将每天4点后第一次运行的数据记为初始值后续再次运行得出数量变化。
每天只自动记录初始化一次,如果需要手动初始化,可以在选项设置中勾选
记录保存30天的数据每次运行保存一次数据
如果当天运行过后,想初始化新料理,可以直接修改料理名称,修改后如果没有本地数据,运行会初始化。
如果新料理今日已初始化,需要手动初始化,可以在选项设置中勾选,勾选后会删除当天的同名记录。
手动运行初始化一次后记得取消勾选,不然运行就一直初始化。
@@ -16,7 +20,7 @@
使用前请确认已装备便携营养袋并且已打开bgi的自动吃药功能。
脚本记录不了烹饪增加的数量,所以如果你打算做菜,建议第一次运行前或最后一次运行后做菜。
脚本记录每次运行读取到的数量,所以如果你打算做菜,建议第一次运行前或最后一次运行后做菜。
账户名只允许使用数字中英文同时长度在20个字符以内。

View File

@@ -19,47 +19,6 @@ const ocrRegion = {
return userName;
}
// 格式化日期为 YYYY/MM/DD
async function formatDate(date) {
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
}
// 获取适用于记录的日期(根据刷新时间调整)
async function getRecordDate() {
const now = new Date();
const currentHour = now.getHours();
// 如果当前时间在刷新时间之前,使用昨天的日期
if (currentHour < 4) {
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
return yesterday;
}
return now;
}
// 处理旧格式记录文件
async function migrateOldFormatRecords(filePath) {
try {
const content = await file.readText(filePath);
const lines = content.split('\n').filter(line => line.trim());
// 检查是否有旧格式的记录如2025-12-10T02:02:32.460Z|179|546
const hasOldFormat = lines.some(line =>
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\|\d+\|\d+$/.test(line)
);
if (hasOldFormat) {
// 直接清空文件(不创建备份)
await file.writeText(filePath, '');
notification.send(`${settings.userName}: 检测到旧格式记录,已重置记录文件`);
return true;
}
} catch (error) {
// 文件不存在或其他错误
}
return false;
}
/**
* 文字OCR识别封装函数支持空文本匹配任意文字
* @param {string} text - 要识别的文字,默认为"空参数",空字符串会匹配任意文字
@@ -192,128 +151,253 @@ const ocrRegion = {
return lastResult || { found: false };
}
/**
* 判断任务是否已刷新
* @param {string} filePath - 存储最后完成时间的文件路径
* @param {object} options - 配置选项
* @param {string} [options.refreshType] - 刷新类型: 'hourly'|'daily'|'weekly'|'monthly'|'custom'
* @param {number} [options.customHours] - 自定义小时数(用于'custom'类型)
* @param {number} [options.dailyHour=4] - 每日刷新的小时(0-23)
* @param {number} [options.weeklyDay=1] - 每周刷新的星期(0-6, 0是周日)
* @param {number} [options.weeklyHour=4] - 每周刷新的小时(0-23)
* @param {number} [options.monthlyDay=1] - 每月刷新的日期(1-31)
* @param {number} [options.monthlyHour=4] - 每月刷新的小时(0-23)
* @returns {Promise<boolean>} - 是否已刷新
*/
async function checkRefreshStatus(filePath, options = {}) {
const {
refreshType = 'daily', // 默认每小时刷新
customHours = 24, // 自定义刷新小时数默认24
dailyHour = 4, // 每日刷新默认凌晨4点
weeklyDay = 1, // 每周刷新默认周一(0是周日)
weeklyHour = 4, // 每周刷新默认凌晨4点
monthlyDay = 1, // 每月刷新默认第1天
monthlyHour = 4 // 每月刷新默认凌晨4点
} = options;
// 首先检查并删除旧格式记录
await migrateOldFormatRecords(filePath);
// 处理错误格式记录文件检测时间格式YYYY/MM/DD HH:mm:ss
async function deleteOldFormatRecords(filePath) {
try {
// 读取文件内容
// 尝试读取文件,不存在则直接返回
const content = await file.readText(filePath);
const lines = content.split('\n').filter(line => line.trim());
if (lines.length === 0) {
return { refreshed: true, recovery: 0, resurrection: 0 };
if (lines.length === 0) return false; // 空文件无需处理
// 时间格式正则:匹配 "时间:YYYY/MM/DD HH:mm:ss" 完整格式
const timeFormatRegex = /时间:\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}/;
// 检查是否所有行都包含正确的时间格式
const allHasValidTime = lines.every(line => timeFormatRegex.test(line));
if (allHasValidTime) return false; // 所有行都有正确时间格式,无需处理
// 存在任意行没有正确时间格式,清空文件
await file.writeText(filePath, '');
notification.send(`${settings.userName}: 检测到记录文件缺少有效时间格式,已重置记录文件`);
return true;
} catch (error) {
// 文件不存在或其他错误时不处理
return false;
}
}
/**
* 获取本地记录中当天4点至次日4点间的最早记录
* @param {string} filePath - 记录文件路径
* @returns {Promise<object>} 包含药品数据的对象
* 格式: { recovery: { count }, resurrection: { count }, initialized: { recovery, resurrection } }
*/
async function getLocalData(filePath) {
// 初始化返回结果
const result = {
recovery: null,
resurrection: null,
initialized: {
recovery: false,
resurrection: false
}
};
try {
// 尝试读取文件,不存在则直接返回空结果
const content = await file.readText(filePath);
const lines = content.split('\n').filter(line => line.trim());
if (lines.length === 0) return result;
// 获取当前时间范围当天4点至次日4点
const now = new Date();
let startTime, endTime;
if (now.getHours() < 4) {
// 当前时间在4点前时间范围为昨天4点至今天4点
startTime = new Date(now);
startTime.setDate(now.getDate() - 1);
startTime.setHours(4, 0, 0, 0);
endTime = new Date(now);
endTime.setHours(4, 0, 0, 0);
} else {
// 当前时间在4点后时间范围为今天4点至明天4点
startTime = new Date(now);
startTime.setHours(4, 0, 0, 0);
endTime = new Date(now);
endTime.setDate(now.getDate() + 1);
endTime.setHours(4, 0, 0, 0);
}
// 解析最新一条记录
const lastLine = lines[lines.length - 1];
const match = lastLine.match(/日期:(\d{4}\/\d{2}\/\d{2})(.+)-(\d+)(.+)-(\d+)/);
// 时间格式正则:匹配 "时间:YYYY/MM/DD HH:mm:ss"
const timeRegex = /时间:(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/;
// 药品匹配正则
const recoveryRegex = new RegExp(`${recoveryFoodName}-(\\d+)`);
const resurrectionRegex = new RegExp(`${resurrectionFoodName}-(\\d+)`);
if (!match) return { refreshed: true, recovery: 0, resurrection: 0 };
// 正向遍历找到第一个小于startTime的行索引边界
let firstOutOfRangeIndex = -1; // 初始化为-1表示所有行都在时间范围内
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const timeMatch = line.match(timeRegex);
if (!timeMatch) continue;
const lastDate = new Date(match[1]);
lastDate.setHours(dailyHour, 0, 0, 0);
const recoveryNum = parseInt(match[3]);
const resurrectionNum = parseInt(match[5]);
const nowTime = new Date();
let shouldRefresh = false;
const recordTime = new Date(timeMatch[1]);
switch (refreshType) {
case 'daily': {// 每天固定时间刷新
// 计算从上次记录到现在的小时数
const diffHours = (nowTime - lastDate) / (1000 * 60 * 60);
// 如果距离上次记录超过24小时或者当前时间在刷新时间之后但日期变化
if (diffHours >= 24 ||
(nowTime.getDate() !== lastDate.getDate() &&
nowTime.getHours() >= dailyHour)) {
shouldRefresh = true;
// 找到第一个超出时间范围小于startTime的行记录索引并终止正向遍历
if (recordTime < startTime) {
// 如果第一条记录时间在今天4点之前直接返回空结果
if (i === 0) {
return result;
}
firstOutOfRangeIndex = i;
break;
}
default:
throw new Error(`未知的刷新类型: ${refreshType}`);
}
return {
refreshed: shouldRefresh,
recovery: recoveryNum,
resurrection: resurrectionNum
};
// 反向遍历的起始索引:如果有超出范围的行,从边界上一行开始;否则从最后一行开始
const reverseStartIndex = firstOutOfRangeIndex === -1
? lines.length - 1
: firstOutOfRangeIndex - 1;
// 反向遍历的终止索引0顶部
const reverseEndIndex = 0;
// 反向遍历:找时间范围内最早的药品记录
// 遍历范围:[reverseStartIndex, reverseEndIndex](从时间范围的最旧→最新)
for (let i = reverseStartIndex; i >= reverseEndIndex; i--) {
// 防止索引越界(比如边界上一行是-1时
if (i < 0) break;
const line = lines[i];
const timeMatch = line.match(timeRegex);
if (!timeMatch) continue;
const recordTime = new Date(timeMatch[1]);
// 二次校验:确保记录在目标时间范围内(避免边界判断误差)
if (recordTime < startTime || recordTime >= endTime) {
continue;
}
// 匹配回血药:未初始化时才赋值
if (!result.initialized.recovery) {
const recoveryMatch = line.match(recoveryRegex);
if (recoveryMatch) {
result.recovery = { count: parseInt(recoveryMatch[1]) };
result.initialized.recovery = true;
}
}
// 匹配复活药:未初始化时才赋值
if (!result.initialized.resurrection) {
const resurrectionMatch = line.match(resurrectionRegex);
if (resurrectionMatch) {
result.resurrection = { count: parseInt(resurrectionMatch[1]) };
result.initialized.resurrection = true;
}
}
// 两个药品都找到,提前终止遍历(已拿到最早记录)
if (result.initialized.recovery && result.initialized.resurrection) {
break;
}
}
return result;
} catch (error) {
// 文件不存在时视为需要刷新
return { refreshed: true, recovery: 0, resurrection: 0 };
// 文件不存在或读取错误时返回空结果
return result;
}
}
// 管理记录文件限制30条
async function updateRecord(filePath, recoveryNum, resurrectionNum) {
const recordDate = await getRecordDate();
const dateStr = await formatDate(recordDate);
const newLine = `日期:${dateStr}${settings.recoveryFoodName}-${recoveryNum}${settings.resurrectionFoodName}-${resurrectionNum}`;
try {
// 首先检查并删除旧格式记录
await migrateOldFormatRecords(filePath);
async function updateRecord(filePath, currentRecovery, currentResurrection, deleteSameDayRecords = false) {
// 生成当前时间字符串
const now = new Date();
const timeStr = `${now.getFullYear()}/${
String(now.getMonth() + 1).padStart(2, '0')
}/${
String(now.getDate()).padStart(2, '0')
} ${
String(now.getHours()).padStart(2, '0')
}:${
String(now.getMinutes()).padStart(2, '0')
}:${
String(now.getSeconds()).padStart(2, '0')
}`;
let content = await file.readText(filePath);
let lines = content.split('\n').filter(line => line.trim());
let updated = false;
// 生成两条新记录
const recoveryLine = `时间:${timeStr}-${recoveryFoodName}-${currentRecovery}`;
const resurrectionLine = `时间:${timeStr}-${resurrectionFoodName}-${currentResurrection}`;
// 检查最后一行是否与要写入的日期相同
if (lines.length > 0) {
const lastLine = lines[lines.length - 1];
try {
let content = await file.readText(filePath);
let lines = content.split('\n').filter(line => line.trim());
// 正则匹配最后一行中的日期
const lastDateMatch = lastLine.match(/日期:(\d{4}\/\d{2}\/\d{2})/);
if (lastDateMatch && lastDateMatch[1] === dateStr) {
// 替换最后一行
lines[lines.length - 1] = newLine;
updated = true;
if (lines.length === 0) {
// 文件为空,直接写入新记录
await file.writeText(filePath, `${recoveryLine}\n${resurrectionLine}`);
return true;
}
}
// 如果日期不同或没有匹配,添加新行
if (!updated) {
lines.push(newLine);
}
// 如果需要删除当天同名记录
if (deleteSameDayRecords) {
// 获取当前时间范围当天4点至次日4点
let startTime, endTime;
if (now.getHours() < 4) {
// 当前时间在4点前时间范围为昨天4点至今天4点
startTime = new Date(now);
startTime.setDate(now.getDate() - 1);
startTime.setHours(4, 0, 0, 0);
endTime = new Date(now);
endTime.setHours(4, 0, 0, 0);
} else {
// 当前时间在4点后时间范围为今天4点至明天4点
startTime = new Date(now);
startTime.setHours(4, 0, 0, 0);
endTime = new Date(now);
endTime.setDate(now.getDate() + 1);
endTime.setHours(4, 0, 0, 0);
}
// 只保留最近30条记录
if (lines.length > 30) {
lines = lines.slice(-30);
}
// 创建药品匹配正则
const recoveryRegex = new RegExp(`${recoveryFoodName}-\\d+$`);
const resurrectionRegex = new RegExp(`${resurrectionFoodName}-\\d+$`);
await file.writeText(filePath, lines.join('\n'));
return true;
} catch (error) {
// 文件不存在时创建新文件
await file.writeText(filePath, newLine);
return true;
// 过滤掉当天时间范围内的同名记录
lines = lines.filter(line => {
const timeMatch = line.match(/时间:(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/);
if (!timeMatch) return true;
const recordTime = new Date(timeMatch[1]);
// 检查是否在当天时间范围内
if (recordTime >= startTime && recordTime < endTime) {
// 检查是否为回血药或复活药记录
if (recoveryRegex.test(line) || resurrectionRegex.test(line)) {
return false; // 删除该记录
}
}
return true; // 保留该记录
});
}
// 添加新记录到最前面
lines.unshift(resurrectionLine);
lines.unshift(recoveryLine);
// 只保留30天内的记录
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentLines = lines.filter(line => {
const timeMatch = line.match(/时间:(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/);
if (!timeMatch) return false;
const lineTime = new Date(timeMatch[1]);
return lineTime >= thirtyDaysAgo;
});
// 写入文件
await file.writeText(filePath, recentLines.join('\n'));
return true;
} catch (error) {
// 文件不存在时创建新文件
await file.writeText(filePath, `${recoveryLine}\n${resurrectionLine}`);
return true;
}
}
}
// 背包过期物品识别需要在背包界面并且是1920x1080分辨率下使用
async function handleExpiredItems() {
@@ -427,65 +511,91 @@ const ocrRegion = {
// 主执行流程
userName = await getUserName();
const recordPath = `assets/${userName}.txt`;
// 检查刷新状态
const refreshStatus = await checkRefreshStatus(recordPath, { dailyHour: 4 });
// 获取当前药物数量
const { recoveryNumber, resurrectionNumber } = await main();
// initSelect处理逻辑
if (settings.initSelect) {
await updateRecord(recordPath, recoveryNumber, resurrectionNumber);
notification.send(`${userName}: 强制初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`);
return;
}
if (refreshStatus.refreshed) {
// 今日未初始化 - 写入初始数量
await updateRecord(recordPath, recoveryNumber, resurrectionNumber);
// 添加账户名称的通知
notification.send(`${userName}: 今日初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`);
// 处理旧的记录文件
await deleteOldFormatRecords(recordPath)
// 获取本地保存的数据
const localData = await getLocalData(recordPath);
// 确定初始化数据
let initRecovery, initResurrection;
let useLocalDataAsInit = false;
if (localData.initialized.recovery && localData.initialized.resurrection) {
// 情况1两者都有
initRecovery = localData.recovery.count;
initResurrection = localData.resurrection.count;
useLocalDataAsInit = true;
log.info(`已读取到本地数据`)
} else if (localData.initialized.recovery || localData.initialized.resurrection) {
// 情况2一有一无用有的那个缺的用当前数据
initRecovery = localData.initialized.recovery ? localData.recovery.count : recoveryNumber;
initResurrection = localData.initialized.resurrection ? localData.resurrection.count : resurrectionNumber;
log.info(`未读取到全部的本地数据,缺失部分使用当前数据作为初始数据`)
} else {
// 使用初始数量进行对比
const initialRecovery = refreshStatus.recovery;
const initialResurrection = refreshStatus.resurrection;
// 计算消耗/增加数量
const diffRecovery = initialRecovery - recoveryNumber;
const diffResurrection = initialResurrection - resurrectionNumber;
let logMsg = "";
// 处理回血药描述
let descRecovery = "";
if (diffRecovery > 0) {
descRecovery = `消耗${recoveryFoodName}${diffRecovery}`;
} else if (diffRecovery < 0) {
descRecovery = `新增${recoveryFoodName}${-diffRecovery}`;
} else {
descRecovery = `${recoveryFoodName}无变化`;
}
// 处理复活药描述
let descResurrection = "";
if (diffResurrection > 0) {
descResurrection = `消耗${resurrectionFoodName}${diffResurrection}`;
} else if (diffResurrection < 0) {
descResurrection = `新增${resurrectionFoodName}${-diffResurrection}`;
} else {
descResurrection = `${resurrectionFoodName}无变化`;
}
// 根据变化组合日志消息
if (diffRecovery === 0 && diffResurrection === 0) {
// 两个值都等于0输出无变化
logMsg = `${userName}: 今日药物数量无变化`;
}else {
// 其他情况
logMsg = `${userName}: 今日${descRecovery}${descResurrection}`;
}
// 添加库存信息
logMsg += ` | 当前库存:${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`;
// 发送通知
notification.send(logMsg);
// 情况3两者都无使用当前数据
initRecovery = recoveryNumber;
initResurrection = resurrectionNumber;
log.info(`未读取到本地数据,使用当前数据作为初始数据`)
}
})();
// 判断是否需要写入两个数据都不为0时才写入
const shouldWriteRecord = recoveryNumber > 0 && resurrectionNumber > 0;
// initSelect处理逻辑
if (settings.initSelect && shouldWriteRecord) {
// 强制初始化:初始化数量和最后一次运行数量都设为当前值
await updateRecord(recordPath, recoveryNumber, resurrectionNumber,deleteSameDayRecords=true);
notification.send(`${userName}: 强制初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`);
return
}
if (shouldWriteRecord) {
// 使用当前的数据更新记录
await updateRecord(recordPath, recoveryNumber, resurrectionNumber);
// 本地有初始记录
if(useLocalDataAsInit){
// 计算消耗/增加数量
const diffRecovery = initRecovery - recoveryNumber;
const diffResurrection = initResurrection - resurrectionNumber;
let logMsg = "";
// 处理回血药描述
let descRecovery = "";
if (diffRecovery > 0) {
descRecovery = `消耗${recoveryFoodName}${diffRecovery}`;
} else if (diffRecovery < 0) {
descRecovery = `新增${recoveryFoodName}${-diffRecovery}`;
} else {
descRecovery = `${recoveryFoodName}无变化`;
}
// 处理复活药描述
let descResurrection = "";
if (diffResurrection > 0) {
descResurrection = `消耗${resurrectionFoodName}${diffResurrection}`;
} else if (diffResurrection < 0) {
descResurrection = `新增${resurrectionFoodName}${-diffResurrection}`;
} else {
descResurrection = `${resurrectionFoodName}无变化`;
}
// 根据变化组合日志消息
if (diffRecovery === 0 && diffResurrection === 0) {
// 两个值都等于0输出无变化
logMsg = `${userName}: 今日药物数量无变化`;
} else {
// 其他情况
logMsg = `${userName}: 今日${descRecovery}${descResurrection}`;
}
// 添加库存信息
logMsg += ` | 当前库存:${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`;
// 发送通知
notification.send(logMsg);
}else{
// 添加账户名称的通知
notification.send(`${userName}: 今日初始化完成!${recoveryFoodName}${initRecovery}个, ${resurrectionFoodName}${initResurrection}`);
}
} else {
// 当前数据有任意一个为0不写入记录只发送通知
notification.send(`${userName}: 当前药品数量识别不全(${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}个),不更新记录`);
}
})();

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "吃药统计",
"version": "1.1",
"version": "1.2",
"bgi_version": "0.51",
"description": "用于统计指定两个食物的消耗,推荐锄地前后使用",
"authors": [