js:采集cd管理2.6.0 (#2488)

同步实验版内容
This commit is contained in:
mno
2025-12-13 17:14:37 +08:00
committed by GitHub
parent 0454df7d52
commit ad374a6767
4 changed files with 895 additions and 304 deletions

View File

@@ -42,6 +42,7 @@
- 根据所给表格结合自身情况选择每个路径组的刷新cd类型 - 根据所给表格结合自身情况选择每个路径组的刷新cd类型
- 选择每个路径组要运行的文件夹 - 选择每个路径组要运行的文件夹
- 填写运行该组使用的配队名称(不填就不换队,有啥用啥) - 填写运行该组使用的配队名称(不填就不换队,有啥用啥)
- 其余自定义配置项参考自定义配置界面的介绍
4. 运行脚本: 4. 运行脚本:
- 运行脚本,脚本将按照路径组的顺序依次执行任务。 - 运行脚本,脚本将按照路径组的顺序依次执行任务。
@@ -54,3 +55,10 @@
- 伪造日志:伪造日志功能仅用于日志分析,不会影响脚本的实际运行。 - 伪造日志:伪造日志功能仅用于日志分析,不会影响脚本的实际运行。
- 识别到满背包的物品将会被加入拾取黑名单,同时加入自定义配置中的 **禁用的路线的关键词** ,将会跳过文件路径包含该关键词的路线,其他你希望跳过的路线也可以使用该配置来跳过,如填写钟离来跳过部分需要钟离的挖矿路线(仅文件路径中包含钟离时有效,还是建议手动删除这些路线) - 识别到满背包的物品将会被加入拾取黑名单,同时加入自定义配置中的 **禁用的路线的关键词** ,将会跳过文件路径包含该关键词的路线,其他你希望跳过的路线也可以使用该配置来跳过,如填写钟离来跳过部分需要钟离的挖矿路线(仅文件路径中包含钟离时有效,还是建议手动删除这些路线)
- 不同配置组中的本js仅共用相同账号的拾取黑名单路线刷新cd路径组等其他信息并不关联settings.json仅供配置每个配置组使用本身变化不影响其他配置组功能 - 不同配置组中的本js仅共用相同账号的拾取黑名单路线刷新cd路径组等其他信息并不关联settings.json仅供配置每个配置组使用本身变化不影响其他配置组功能
六、进阶配置
-
1. 在自定义配置中勾选高级配置后展开即可查看
2. 优先采集材料:按规则填写后,每天会尝试获取指定数量的材料后再进入路径组模式,注意,需要有足够多的拾取历史或文件路径包含目标材料的路径,其余路线无法被识别。
3. 优先关键词:在路径组模式中,含有这些关键词的路线会被视为最高效率,配合效率降序排序或者最低效率配置项使用
4. 路径组临界效率执行路径组时分均效率低于指定值的路线会被排除特别的无拾取记录或记录少于3次的路线会被视为恰好处于该临界不会被排除

View File

@@ -0,0 +1,58 @@
{
"info": {
"name": "A00-塞洛海原(学习螃蟹技能)",
"type": "collect",
"authors": [
{
"name": "芝士贝果",
"links": "https://github.com/cheese-bagel"
}
],
"version": "1.0",
"description": "",
"map_name": "Teyvat",
"bgi_version": "0.50.0",
"tags": [],
"last_modified_time": 1755574896298,
"enable_monster_loot_split": false,
"map_match_method": ""
},
"positions": [
{
"id": 1,
"x": 4399.8906,
"y": 3083.4001,
"action": "",
"move_mode": "walk",
"action_params": "",
"type": "teleport"
},
{
"id": 2,
"x": 4421.7334,
"y": 3081.2798,
"action": "combat_script",
"move_mode": "walk",
"action_params": "attack;wait(0.5);moveby(0,2285);wait(0.5);attack;click(middle)",
"type": "orientation"
},
{
"id": 3,
"x": 4413.1172,
"y": 3106.9414,
"type": "orientation",
"move_mode": "walk",
"action": "combat_script",
"action_params": "attack;wait(0.5);moveby(0,2285);wait(0.5);attack;click(middle)"
},
{
"id": 4,
"x": 4390.1826,
"y": 3099.9868,
"action": "combat_script",
"move_mode": "walk",
"action_params": "attack;wait(0.5);moveby(0,2285);wait(0.5);attack",
"type": "orientation"
}
]
}

View File

@@ -19,6 +19,9 @@ const accountName = settings.infoFileName || "默认账户";
// 定义目标文件夹路径和记录文件路径 // 定义目标文件夹路径和记录文件路径
const recordFolder = "record"; // 存储记录文件的文件夹路径 const recordFolder = "record"; // 存储记录文件的文件夹路径
const defaultTimeStamp = "2023-10-13T00:00:00.000Z"; // 固定的时间戳 const defaultTimeStamp = "2023-10-13T00:00:00.000Z"; // 固定的时间戳
let pickupRecordFile;
const MAX_PICKUP_DAYS = 30;
// 从 settings 中读取用户配置,并设置默认值 // 从 settings 中读取用户配置,并设置默认值
const userSettings = { const userSettings = {
@@ -95,6 +98,8 @@ FiconRo.InitTemplate();
const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150); const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150);
let underWater = false;
(async function () { (async function () {
/* ===== 零基构建 settings.jsonBEGIN ===== */ /* ===== 零基构建 settings.jsonBEGIN ===== */
const SETTINGS_FILE = `settings.json`; const SETTINGS_FILE = `settings.json`;
@@ -205,6 +210,16 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
if (settings.enableMoreSettings) { if (settings.enableMoreSettings) {
newSettings.push( newSettings.push(
{
"name": "priorityItems",
"type": "input-text",
"label": "优先采集材料,每天会尝试优先采集指定数量的目标物品,随后才执行路径组\n格式材料名*数量,由加号+连接\n如萃凝晶*160+甜甜花*10"
},
{
"name": "priorityItemsPartyName",
"type": "input-text",
"label": "优先采集材料使用的配队名称"
},
{ {
"name": "priorityTags", "name": "priorityTags",
"type": "input-text", "type": "input-text",
@@ -221,6 +236,12 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
], ],
"default": "文件顺序,按在文件夹中位置顺序运行" "default": "文件顺序,按在文件夹中位置顺序运行"
}, },
{
"name": "defaultEffPercentile",
"type": "input-text",
"label": "默认效率指数范围0-1\n数值越大时未知效率的路线被视作的默认效率越高",
"default": "0.5"
},
{ {
"name": "weightedRule", "name": "weightedRule",
"type": "input-text", "type": "input-text",
@@ -267,7 +288,7 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
newSettings.push({ newSettings.push({
"name": `pathGroup${g}thresholdEfficiency`, "name": `pathGroup${g}thresholdEfficiency`,
"type": "input-text", "type": "input-text",
"label": `路径组${g}临界效率\n分均拾取个数效率低于临界效率的路线会被排除\n无历史记录或历史记录少于3次的路线会被视为恰好处于临界效率`, "label": `路径组${g}临界效率\n分均拾取个数效率低于临界效率的路线会被排除`,
"default": "0" "default": "0"
}); });
} }
@@ -298,6 +319,7 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
// 获取子文件夹路径 // 获取子文件夹路径
const subFolderName = userSettings.infoFileName; const subFolderName = userSettings.infoFileName;
const subFolderPath = `${recordFolder}/${subFolderName}`; const subFolderPath = `${recordFolder}/${subFolderName}`;
pickupRecordFile = `${recordFolder}/${subFolderName}/拾取记录.json`;
// 读取子文件夹中的所有文件路径 // 读取子文件夹中的所有文件路径
const filesInSubFolder = file.ReadPathSync(subFolderPath); const filesInSubFolder = file.ReadPathSync(subFolderPath);
@@ -327,6 +349,14 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
/* 禁用BGI原生拾取强制模板匹配 */ /* 禁用BGI原生拾取强制模板匹配 */
targetItems = await loadTargetItems(); targetItems = await loadTargetItems();
/* ===== 别名索引 ===== */
const name2Other = new Map(); // 本名 → 别名数组
const other2Name = new Map(); // 别名 → 本名
for (const it of targetItems) {
name2Other.set(it.itemName, it.otherName || []);
for (const a of (it.otherName || [])) other2Name.set(a, it.itemName);
}
await loadBlacklist(true); await loadBlacklist(true);
state.running = true; state.running = true;
@@ -408,9 +438,435 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
let cookInterval = 60 * 60 * 1000; let cookInterval = 60 * 60 * 1000;
let settimeInterval = 10 * 60 * 1000; let settimeInterval = 10 * 60 * 1000;
// ==================== 优先级材料前置采集 ====================
if (settings.priorityItems) {
/* ---------- 1. 解析 ---------- */
const priorityList = [];
const segments = settings.priorityItems.split('+').map(s => s.trim());
for (const seg of segments) {
const [itemName, countStr] = seg.split('*').map(s => s.trim());
if (itemName && countStr && !isNaN(Number(countStr))) {
priorityList.push({ itemName, count: Number(countStr) });
}
}
log.info(`优先级材料解析完成: ${priorityList.map(e => `${e.itemName}*${e.count}`).join(', ')}`);
/* ===== 追加扣除今日已拾取UTC+8 0 点分界) ===== */
const utc8 = new Date(Date.now() + 8 * 3600_000); // 手动+8小时
const today = utc8.toISOString().slice(0, 10); // "YYYY-MM-DD"
let todayPicked = {}; // 今日已拾取数量
try {
const txt = await file.readText(pickupRecordFile);
if (txt) {
const arr = JSON.parse(txt);
const todayItem = arr.find(it => it.date === today);
if (todayItem) todayPicked = todayItem.items || {};
}
} catch (_) { /* 文件不存在或解析失败 */ }
/* 扣除今日已拾取:别名→本名 */
for (let i = priorityList.length - 1; i >= 0; i--) {
const task = priorityList[i];
let got = 0;
/* 先算本名 */
got += todayPicked[task.itemName] || 0;
/* 再算别名 */
const others = name2Other.get(task.itemName) || [];
for (const a of others) got += todayPicked[a] || 0;
task.count -= got;
if (task.count <= 0) priorityList.splice(i, 1);
}
if (priorityList.length === 0) {
log.info("今日优先材料已达标,跳过优先采集阶段");
}
/* ================================= */
/* ---------- 2. 材料→CD类型 映射表(仅列出现过的,其余默认 1次0点刷新---------- */
const materialCdMap = {
// 46h 特产
"小灯草": "46小时刷新",
"嘟嘟莲": "46小时刷新",
"落落莓": "46小时刷新",
"塞西莉亚花": "46小时刷新",
"慕风蘑菇": "46小时刷新",
"蒲公英籽": "46小时刷新",
"钩钩果": "46小时刷新",
"风车菊": "46小时刷新",
"霓裳花": "46小时刷新",
"清心": "46小时刷新",
"琉璃袋": "46小时刷新",
"琉璃百合": "46小时刷新",
"夜泊石": "46小时刷新",
"绝云椒椒": "46小时刷新",
"星螺": "46小时刷新",
"石珀": "46小时刷新",
"清水玉": "46小时刷新",
"海灵芝": "46小时刷新",
"鬼兜虫": "46小时刷新",
"绯樱绣球": "46小时刷新",
"鸣草": "46小时刷新",
"珊瑚真珠": "46小时刷新",
"晶化骨髓": "46小时刷新",
"血斛": "46小时刷新",
"天云草实": "46小时刷新",
"幽灯蕈": "46小时刷新",
"沙脂蛹": "46小时刷新",
"月莲": "46小时刷新",
"帕蒂沙兰": "46小时刷新",
"树王圣体菇": "46小时刷新",
"圣金虫": "46小时刷新",
"万相石": "46小时刷新",
"悼灵花": "46小时刷新",
"劫波莲": "46小时刷新",
"赤念果": "46小时刷新",
"苍晶螺": "46小时刷新",
"海露花": "46小时刷新",
"柔灯铃": "46小时刷新",
"子探测单元": "46小时刷新",
"湖光铃兰": "46小时刷新",
"幽光星星": "46小时刷新",
"虹彩蔷薇": "46小时刷新",
"初露之源": "46小时刷新",
"浪沫羽鳃": "46小时刷新",
"灼灼彩菊": "46小时刷新",
"肉龙掌": "46小时刷新",
"青蜜莓": "46小时刷新",
"枯叶紫英": "46小时刷新",
"微光角菌": "46小时刷新",
"云岩裂叶": "46小时刷新",
"琉鳞石": "46小时刷新",
"奇异的「牙齿」": "46小时刷新",
// 12h 素材
"兽肉": "12小时刷新",
"禽肉": "12小时刷新",
"神秘的肉": "12小时刷新",
"鱼肉": "12小时刷新",
"鳗肉": "12小时刷新",
"螃蟹": "12小时刷新",
"蝴蝶翅膀": "12小时刷新",
"青蛙": "12小时刷新",
"发光髓": "12小时刷新",
"蜥蜴尾巴": "12小时刷新",
"晶核": "12小时刷新",
"鳅鳅宝玉": "12小时刷新",
// 4点
"盐": "1次4点刷新",
"胡椒": "1次4点刷新",
"洋葱": "1次4点刷新",
"牛奶": "1次4点刷新",
"番茄": "1次4点刷新",
"卷心菜": "1次4点刷新",
"土豆": "1次4点刷新",
"小麦": "1次4点刷新",
"稻米": "1次4点刷新",
"虾仁": "1次4点刷新",
"豆腐": "1次4点刷新",
"杏仁": "1次4点刷新",
"发酵果实汁": "1次4点刷新",
"咖啡豆": "1次4点刷新",
"秃秃豆": "1次4点刷新",
// 0点
"甜甜花": "1次0点刷新",
"胡萝卜": "1次0点刷新",
"蘑菇": "1次0点刷新",
"松茸": "1次0点刷新",
"松果": "1次0点刷新",
"金鱼草": "1次0点刷新",
"莲蓬": "1次0点刷新",
"薄荷": "1次0点刷新",
"鸟蛋": "1次0点刷新",
"树莓": "1次0点刷新",
"白萝卜": "1次0点刷新",
"苹果": "1次0点刷新",
"日落果": "1次0点刷新",
"竹笋": "1次0点刷新",
"海草": "1次0点刷新",
"堇瓜": "1次0点刷新",
"星蕈": "1次0点刷新",
"墩墩桃": "1次0点刷新",
"须弥蔷薇": "1次0点刷新",
"香辛果": "1次0点刷新",
"枣椰": "1次0点刷新",
"泡泡桔": "1次0点刷新",
"汐藻": "1次0点刷新",
"茉洁草": "1次0点刷新",
"久雨莲": "1次0点刷新",
"沉玉仙茗": "24小时刷新",
"颗粒果": "1次0点刷新",
"烛伞蘑菇": "1次0点刷新",
"澄晶实": "1次0点刷新",
"红果果菇": "1次0点刷新",
"马尾": "1次0点刷新",
"烈焰花花蕊": "1次0点刷新",
"铁块": "1次0点刷新",
"白铁块": "2次0点刷新",
"星银矿石": "2次0点刷新",
"水晶块": "3次0点刷新",
"紫晶块": "3次0点刷新",
"萃凝晶": "3次0点刷新",
"虹滴晶": "3次0点刷新",
"苦种": "1次0点刷新",
"烬芯花": "1次0点刷新"
};
let changedParty = false;
/* ---------- 3. 主循环 ---------- */
while (priorityList.length > 0) {
const priorityItemSet = new Set(priorityList.map(p => p.itemName));
for (const a of priorityItemSet) {
const others = name2Other.get(a) || [];
for (const o of others) priorityItemSet.add(o); // 别名也加入
}
const pickedCounter = {};
priorityItemSet.forEach(n => pickedCounter[n] = 0);
/* ===== 每轮开始输出剩余物品 ===== */
log.info(`剩余目标材料 ${priorityList.map(t => `${t.itemName}*${t.count}`).join(', ')}`);
/* 4-1 扫描 + 读 record + 前置过滤(禁用/时间/材料相关)+ 计算效率 + CD后置排除 */
const allFiles = await readFolder('pathing', true);
const rawRecord = await file.readText(`${recordFolder}/${subFolderName}/record.json`);
let recordArray = [];
try { recordArray = JSON.parse(rawRecord); } catch { /* 空记录 */ }
const cdMap = new Map(recordArray.map(it => [it.fileName, it]));
const now = new Date();
/* 时间管制 */
if (await isTimeRestricted(settings.timeRule, 10)) { priorityList.length = 0; break; }
/* ---- 先算效率不判CD---- */
for (const file of allFiles) {
const fullName = file.fileName;
const rec = cdMap.get(fullName);
/* 禁用关键词 */
let skip = false;
for (const kw of disableArray) { if (file.fullPath.includes(kw)) { skip = true; break; } }
if (skip) { file._priorityEff = -1; continue; }
/* 材料相关 */
const pathHit = [...priorityItemSet].some(n => file.fullPath.includes(n));
const histHit = rec?.history?.some(log =>
Object.keys(log.items).some(name => priorityItemSet.has(name))
) ?? false;
let descHit = false;
if (file.description) {
descHit = [...priorityItemSet].some(kw => file.description.includes(kw));
}
if (!pathHit && !histHit && !descHit) {
file._priorityEff = -1;
continue;
}
/* 计算仅看优先材料的分均效率 */
let eff = -2; // 未知标记
if (rec?.history && rec.history.length >= 3) {
const effList = rec.history.map(log => {
const total = Object.entries(log.items)
.filter(([name]) => priorityItemSet.has(name))
.reduce((sum, [, cnt]) => sum + cnt, 0);
return (total / log.durationSec) * 60;
});
eff = effList.reduce((a, b) => a + b, 0) / effList.length;
}
file._priorityEff = eff;
}
/* ---- 用可运行路线算分位默认值 ---- */
const knownEff = allFiles
.filter(f => {
const rec = cdMap.get(f.fileName);
const nextCD = rec ? new Date(rec.cdTime) : new Date(0);
return f._priorityEff >= 0 && now > nextCD;
})
.map(f => f._priorityEff)
.sort((a, b) => a - b);
let defaultEff;
if (knownEff.length === 0) {
defaultEff = 1;
} else {
const rawPct = settings.defaultEffPercentile;
const pct = Math.max(0, Math.min(1, rawPct === "" ? 0.5 : Number(rawPct)));
const idx = Math.ceil(pct * knownEff.length) - 1;
defaultEff = knownEff[Math.max(0, idx)];
}
/* 回填未知 + 排除CD */
allFiles.forEach(f => {
if (f._priorityEff === -2) f._priorityEff = defaultEff;
const rec = cdMap.get(f.fileName);
const nextCD = rec ? new Date(rec.cdTime) : new Date(0);
if (now <= nextCD) f._priorityEff = -1;
});
if (priorityList.length === 0) break;
/* 4-2 只跑最高效率路线 */
const candidateRoutes = allFiles.filter(f => f._priorityEff >= 0)
.sort((a, b) => b._priorityEff - a._priorityEff);
if (candidateRoutes.length === 0) {
log.info('已无可用优先路线可能全部在CD或已达标退出优先采集阶段');
break;
}
const bestRoute = candidateRoutes[0];
const filePath = bestRoute.fullPath;
const fileName = basename(filePath).replace('.json', '');
const fullName = fileName + '.json';
const targetObj = cdMap.get(fullName);
const startTime = new Date();
if (!changedParty && settings.priorityItemsPartyName) {
await switchPartyIfNeeded(settings.priorityItemsPartyName);
changedParty = true;
}
let timeNow = new Date();
if (Foods.length != 0 && (((timeNow - lastCookTime) > cookInterval) || firstCook)) {
firstCook = false;
await ingredientProcessing();
lastCookTime = new Date();
}
if (settings.setTimeMode && settings.setTimeMode != "不调节时间" && (((timeNow - lastsettimeTime) > settimeInterval) || firstsettime)) {
firstsettime = false;
if (settings.setTimeMode === "尽量调为白天") {
await pathingScript.runFile("assets/调为白天.json");
} else {
await pathingScript.runFile("assets/调为夜晚.json");
}
lastsettimeTime = new Date();
}
await fakeLog(fileName, false, true, 0);
/* ================================= */
log.info(`当前进度:执行路线 ${fileName}`);
state.running = true;
const pickupTask = recognizeAndInteract();
if (!underWater && filePath.includes('枫丹水下')) {
await pathingScript.runFile("assets/A00-塞洛海原(学习螃蟹技能).json");
underWater = true;
}
if (underWater && !filePath.includes('枫丹水下')) {
underWater = false;
}
try {
await pathingScript.runFile(filePath);
} catch (e) {
log.error(`优先采集路线执行失败: ${filePath}`);
state.running = false; await pickupTask; continue;
}
state.running = false; await pickupTask;
await fakeLog(fileName, false, false, 0);
/* 4-3 扣除进度:别名→本名 */
state.runPickupLog.forEach(name => {
const realName = other2Name.get(name) || name; // 别名→本名
if (priorityItemSet.has(name) || priorityItemSet.has(realName)) {
pickedCounter[realName] = (pickedCounter[realName] || 0) + 1;
}
});
/* ===== 追加:立即把 pickedCounter 回写到 priorityList ===== */
for (const task of priorityList) {
const left = task.count - (pickedCounter[task.itemName] || 0);
task.count = Math.max(0, left); // 防止负数
}
/* 倒序删除已达标项 */
for (let i = priorityList.length - 1; i >= 0; i--) {
if (priorityList[i].count <= 0) {
log.info(`优先材料已达标: ${priorityList[i].itemName}`);
priorityList.splice(i, 1);
}
}
/* ================================================ */
/* 4-4 计算CD掉落材料决定*/
const timeDiff = new Date() - startTime;
if (timeDiff > 3000) {
/* 1) 如果runPickupLog中不含优先材料则按其他材料查找使用最晚刷新时间 */
let hasPriority = state.runPickupLog.some(name => priorityItemSet.has(name));
let hitMaterials;
if (hasPriority) {
hitMaterials = [...new Set(state.runPickupLog.filter(n => priorityItemSet.has(n)))];
} else {
/* 非优先材料也按同一张表查CD */
hitMaterials = [...new Set(state.runPickupLog)];
}
let latestCD = new Date(0); // 初始极小值
let foundAny = false;
hitMaterials.forEach(name => {
const cdType = materialCdMap[name] || "1次0点刷新";
let tmpDate = new Date(startTime);
switch (cdType) {
case "1次0点刷新":
tmpDate.setDate(tmpDate.getDate() + 1);
tmpDate.setHours(0, 0, 0, 0);
break;
case "2次0点刷新":
tmpDate.setDate(tmpDate.getDate() + 2);
tmpDate.setHours(0, 0, 0, 0);
break;
case "3次0点刷新":
tmpDate.setDate(tmpDate.getDate() + 3);
tmpDate.setHours(0, 0, 0, 0);
break;
case "1次4点刷新":
tmpDate.setHours(4, 0, 0, 0);
if (tmpDate <= startTime) tmpDate.setDate(tmpDate.getDate() + 1);
break;
case "12小时刷新":
tmpDate = new Date(startTime.getTime() + 12 * 60 * 60 * 1000);
break;
case "24小时刷新":
tmpDate = new Date(startTime.getTime() + 24 * 60 * 60 * 1000);
break;
case "46小时刷新":
tmpDate = new Date(startTime.getTime() + 46 * 60 * 60 * 1000);
break;
default:
tmpDate.setDate(tmpDate.getDate() + 1);
tmpDate.setHours(0, 0, 0, 0);
}
if (tmpDate > latestCD) latestCD = tmpDate;
foundAny = true;
});
/* 兜底没有任何材料被识别到按1次0点刷新 */
if (!foundAny) {
latestCD = new Date(startTime);
latestCD.setDate(latestCD.getDate() + 1);
latestCD.setHours(0, 0, 0, 0);
}
const durationSec = Math.round(timeDiff / 1000);
const itemCounter = {};
state.runPickupLog.forEach(n => { itemCounter[n] = (itemCounter[n] || 0) + 1; });
if (!targetObj.history) targetObj.history = [];
targetObj.history.push({ items: itemCounter, durationSec });
if (targetObj.history.length > 7) targetObj.history = targetObj.history.slice(-7);
targetObj.cdTime = latestCD.toISOString();
await file.writeText(recordFilePath,
JSON.stringify(Array.from(cdMap.values()), null, 2));
await appendDailyPickup(state.runPickupLog);
state.runPickupLog = [];
}
}
}
let runnedAnyPath = true;
// ==================== 路径组循环 ==================== // ==================== 路径组循环 ====================
while (runnedAnyPath) {
runnedAnyPath = false;
if (await isTimeRestricted(settings.timeRule, 10)) break;
for (let i = 1; i <= groupCount; i++) { for (let i = 1; i <= groupCount; i++) {
if (await isTimeRestricted(settings.timeRule, 10)) break;
const currentCdType = settings[`pathGroup${i}CdType`] || ""; const currentCdType = settings[`pathGroup${i}CdType`] || "";
if (!currentCdType) continue; if (!currentCdType) continue;
@@ -453,11 +909,11 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
}); });
} }
// 2) 先计算一次基础效率,并找出全局最大效率 // 2) 先计算一次基础效率(未知路线先标 -1
filePaths.forEach(p => { filePaths.forEach(p => {
const fullName = basename(p); const fullName = basename(p);
const obj = cdMap.get(fullName); const obj = cdMap.get(fullName);
let avgEff = 0; let avgEff = -1; // 先标记为“未知”
if (obj && obj.history && obj.history.length >= 3) { if (obj && obj.history && obj.history.length >= 3) {
const effList = obj.history.map(log => { const effList = obj.history.map(log => {
@@ -468,37 +924,54 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
return (total / log.durationSec) * 60; return (total / log.durationSec) * 60;
}); });
avgEff = effList.reduce((a, b) => a + b, 0) / effList.length; avgEff = effList.reduce((a, b) => a + b, 0) / effList.length;
} else {
const threshold = Number(settings[`pathGroup${i}thresholdEfficiency`]) || 0;
avgEff = threshold;
} }
p._efficiency = avgEff; // 先存基础值 p._efficiency = avgEff; // 已知路线存真实效率,未知路线存 -1
}); });
// 3) 计算全局最大效率值 // 3) 计算默认效率(分位值)
const knownEff = filePaths
.map(p => p._efficiency)
.filter(e => e >= 0) // 只保留已知路线
.sort((a, b) => a - b);
let defaultEff;
if (knownEff.length === 0) {
// 一条已知路线都没有 → 回退到老逻辑
defaultEff = Number(settings[`pathGroup${i}thresholdEfficiency`]) || 0;
} else {
// 按配置的分位取默认效率
const rawPct = settings.defaultEffPercentile;
const pct = Math.max(0, Math.min(1, rawPct === "" ? 0.5 : Number(rawPct)));
const idx = Math.ceil(pct * knownEff.length) - 1;
defaultEff = knownEff[Math.max(0, idx)];
}
// 4) 把 -1 的未知路线替换成默认效率
filePaths.forEach(p => {
if (p._efficiency === -1) p._efficiency = defaultEff;
});
// 5) 计算全局最大效率值(已含默认效率)
const maxEff = Math.max(...filePaths.map(p => p._efficiency), 0); const maxEff = Math.max(...filePaths.map(p => p._efficiency), 0);
// 4) 优先关键词加分 // 6) 优先关键词加分(逻辑不变)
filePaths.forEach(p => { filePaths.forEach(p => {
const fullName = basename(p); const fullName = basename(p);
const obj = cdMap.get(fullName); const obj = cdMap.get(fullName);
// 4-1) 历史拾取物里是否含关键词
const itemHit = obj?.history?.some(log => const itemHit = obj?.history?.some(log =>
Object.keys(log.items).some(item => Object.keys(log.items).some(item =>
priorityKeywords.some(key => item.includes(key)) priorityKeywords.some(key => item.includes(key))
) )
); );
// 4-2) 文件路径(含文件名)是否含关键词
const pathHit = priorityKeywords.some(key => p.includes(key)); const pathHit = priorityKeywords.some(key => p.includes(key));
const descHit = priorityKeywords.some(key => (p.description || '').includes(key));
if (itemHit || pathHit) { if (itemHit || pathHit || descHit) {
p._efficiency += maxEff; // 把最大效率值直接加给它 p._efficiency += maxEff + 1;
} }
}); });
/* ================== 排序分支 ================== */ /* ================== 排序分支 ================== */
switch (settings.sortMode) { switch (settings.sortMode) {
case "优先最早刷新,将优先执行最早刷新的路线": case "优先最早刷新,将优先执行最早刷新的路线":
@@ -597,9 +1070,16 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
state.running = true; state.running = true;
const pickupTask = recognizeAndInteract(); const pickupTask = recognizeAndInteract();
runnedAnyPath = true;
log.info(`当前进度:路径组${i} ${folder} ${fileName} 为第 ${filePaths.indexOf(filePath) + 1}/${filePaths.length}`); log.info(`当前进度:路径组${i} ${folder} ${fileName} 为第 ${filePaths.indexOf(filePath) + 1}/${filePaths.length}`);
if (!underWater && filePath.includes('枫丹水下')) {
await pathingScript.runFile("assets/A00-塞洛海原(学习螃蟹技能).json");
underWater = true;
}
if (underWater && !filePath.includes('枫丹水下')) {
underWater = false;
}
try { try {
state.runPickupLog = []; // 新路线开始前清空 state.runPickupLog = []; // 新路线开始前清空
await pathingScript.runFile(filePath); await pathingScript.runFile(filePath);
@@ -666,6 +1146,7 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
targetObj.cdTime = newTimestamp.toISOString(); targetObj.cdTime = newTimestamp.toISOString();
await file.writeText(recordFilePath, await file.writeText(recordFilePath,
JSON.stringify(Array.from(cdMap.values()), null, 2)); JSON.stringify(Array.from(cdMap.values()), null, 2));
await appendDailyPickup(state.runPickupLog);
// 清空本次记录 // 清空本次记录
state.runPickupLog = []; state.runPickupLog = [];
@@ -679,6 +1160,7 @@ const mainUiRo = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150)
} }
} }
} }
}
} catch (error) { } catch (error) {
log.error(`操作失败: ${error}`); log.error(`操作失败: ${error}`);
@@ -910,14 +1392,18 @@ async function loadTargetItems() {
it.roi.Threshold = itsThreshold; it.roi.Threshold = itsThreshold;
it.roi.InitTemplate(); it.roi.InitTemplate();
/* ---------- 2. 解析中括号内容 ---------- */ /* ---------- 2. 解析中括号内容 + 纯中文过滤 ---------- */
const otherNames = []; const otherNames = new Set(); // 用 Set 去重
const bracketMatch = it.fullPath.matchAll(/\[(.*?)\]/g); // 2-1 中括号匹配
for (const m of bracketMatch) { for (const m of it.fullPath.matchAll(/\[(.*?)\]/g)) {
if (m[1].trim()) otherNames.push(m[1].trim()); const pure = (m[1] || '').replace(/[^\u4e00-\u9fff]/g, '').trim();
if (pure) otherNames.add(pure);
} }
it.otherName = otherNames; // 始终返回数组,无则为空数组 // 2-2 若 itemName 本身含非中文,也生成纯中文别名
const namePure = it.itemName.replace(/[^\u4e00-\u9fff]/g, '').trim();
if (namePure && namePure !== it.itemName) otherNames.add(namePure);
it.otherName = Array.from(otherNames); // 转回数组
} catch (error) { } catch (error) {
log.error(`[loadTargetItems] ${it.fullPath}: ${error.message}`); log.error(`[loadTargetItems] ${it.fullPath}: ${error.message}`);
} }
@@ -1052,57 +1538,55 @@ function removeJsonSuffix(fileName) {
// 定义 readFolder 函数 // 定义 readFolder 函数
async function readFolder(folderPath, onlyJson) { async function readFolder(folderPath, onlyJson) {
// 新增一个堆栈,初始时包含 folderPath
const folderStack = [folderPath]; const folderStack = [folderPath];
// 新增一个数组,用于存储文件信息对象
const files = []; const files = [];
// 当堆栈不为空时,继续处理
while (folderStack.length > 0) { while (folderStack.length > 0) {
// 从堆栈中弹出一个路径
const currentPath = folderStack.pop(); const currentPath = folderStack.pop();
const filesInSubFolder = file.ReadPathSync(currentPath); // 同步读取
// 读取当前路径下的所有文件和子文件夹路径
const filesInSubFolder = file.ReadPathSync(currentPath);
// 临时数组,用于存储子文件夹路径
const subFolders = []; const subFolders = [];
for (const filePath of filesInSubFolder) { for (const filePath of filesInSubFolder) {
if (file.IsFolder(filePath)) { if (file.IsFolder(filePath)) {
// 如果是文件夹,先存储到临时数组中
subFolders.push(filePath); subFolders.push(filePath);
} else {
if (filePath.endsWith(".js")) {
//跳过js结尾的文件
continue; continue;
} }
// 如果是文件,根据 onlyJson 判断是否存储
if (filePath.endsWith('.js')) continue; // 跳过 js
// 仅 json 模式
if (onlyJson) { if (onlyJson) {
if (filePath.endsWith(".json")) { if (!filePath.endsWith('.json')) continue;
const fileName = filePath.split('\\').pop(); // 提取文件名
const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组 let description = '';
try {
// 同步读文本,避免 async 传染
const txt = file.readTextSync(filePath);
const parsed = JSON.parse(txt);
description = parsed?.info?.description ?? '';
} catch {
/* 读盘或解析失败就留空串 */
}
const fileName = filePath.split('\\').pop();
const folderPathArray = filePath.split('\\').slice(0, -1);
files.push({ files.push({
fullPath: filePath, fullPath: filePath,
fileName: fileName, fileName,
folderPathArray: folderPathArray folderPathArray,
description
}); });
//log.info(`找到 JSON 文件:${filePath}`); continue;
} }
} else {
const fileName = filePath.split('\\').pop(); // 提取文件名 const fileName = filePath.split('\\').pop();
const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组 const folderPathArray = filePath.split('\\').slice(0, -1);
files.push({ files.push({ fullPath: filePath, fileName, folderPathArray });
fullPath: filePath,
fileName: fileName,
folderPathArray: folderPathArray
});
//log.info(`找到文件:${filePath}`);
} }
}
} // 子文件夹按原顺序入栈(深度优先)
// 将临时数组中的子文件夹路径按原顺序压入堆栈 folderStack.push(...subFolders.reverse());
folderStack.push(...subFolders.reverse()); // 反转子文件夹路径
} }
return files; return files;
@@ -1193,8 +1677,6 @@ async function isTimeRestricted(timeRule, threshold = 5) {
return true; return true;
} }
} }
log.info("不处于限制时间");
return false; return false;
} }
@@ -1474,3 +1956,46 @@ async function ingredientProcessing() {
} }
await genshin.returnMainUi(); await genshin.returnMainUi();
} }
/**
* 把本次路线的掉落合并到“拾取记录.json”中同一天条目不含 durationSec
* @param {string[]} pickupLog 本次路线的 state.runPickupLog
*/
async function appendDailyPickup(pickupLog) {
if (!pickupLog || !pickupLog.length) return;
let oldArr = [];
try {
const txt = await file.readText(pickupRecordFile);
if (txt) oldArr = JSON.parse(txt);
} catch (_) { /* 文件不存在或解析失败 */ }
// 统一按 UTC+8 的 0 点划分日期
const utc8 = new Date(Date.now() + 8 * 3600_000);
const today = utc8.toISOString().slice(0, 10); // "YYYY-MM-DD"
let todayItem = oldArr.find(e => e.date === today);
if (!todayItem) {
todayItem = { date: today, items: {} };
oldArr.push(todayItem);
}
const todayItems = todayItem.items;
pickupLog.forEach(name => {
todayItems[name] = (todayItems[name] || 0) + 1;
});
// 滑动窗口:只保留最近 MAX_PICKUP_DAYS 天
if (oldArr.length > MAX_PICKUP_DAYS) oldArr = oldArr.slice(-MAX_PICKUP_DAYS);
// 按日期倒序(最新在前)
oldArr.sort((a, b) => b.date.localeCompare(a.date));
// 写盘 + 异常捕获
try {
await file.writeText(pickupRecordFile, JSON.stringify(oldArr, null, 2), false);
} catch (error) {
log.error(`appendDailyPickup 写盘失败: ${error.message}`);
}
}

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 1, "manifest_version": 1,
"name": "采集cd管理", "name": "采集cd管理",
"version": "2.4.1", "version": "2.6.0",
"bgi_version": "0.44.8", "bgi_version": "0.44.8",
"description": "仅面对会操作文件和读readme的用户基于文件夹操作自动管理采集路线的cd会按照路径组的顺序依次运行直到指定的时间并会按照给定的cd类型自动跳过未刷新的路线", "description": "仅面对会操作文件和读readme的用户基于文件夹操作自动管理采集路线的cd会按照路径组的顺序依次运行直到指定的时间并会按照给定的cd类型自动跳过未刷新的路线",
"saved_files": [ "saved_files": [