feat: 补全莉奈娅挖矿工具链 (#3203)

* feat: 优化路径

* feat: 优化路径

* feat: 矿石数量检测

* feat: 临时注释背包检测

* fix: 修复问题

* feat: 优化路线

* feat: 优化路线
This commit is contained in:
躁动的氨气
2026-05-10 13:32:15 +08:00
committed by GitHub
parent c14938b648
commit ebb4a65b6a
62 changed files with 714 additions and 282 deletions

View File

@@ -1,6 +1,8 @@
# 莉奈娅挖矿一条龙
专用于运行莉奈娅自瞄挖矿的脚本,不分矿种,只挖稳定刷新的矿物。
专用于运行莉奈娅自瞄挖矿的脚本,不分矿种,只挖稳定刷新的矿物。
自带月卡监听可跨4点执行但月卡跳出时脚本仍在执行可能会引起故障。
本脚本不会提供过多设置项,仅保留必要设置,最大程度限制设置数量。
<h1 style="color: red">当前脚本仅为先行版,占位避免撞车,出现异常为正常现象</h1>
@@ -18,18 +20,17 @@
## 说明
说在最开始:莉奈娅挖矿需要识别元素视野下的黄色岩元素属性矿物,原神设置项开的过低会导致黄色变浅,识别率下降,在足够运行的情况下请不要为了节省微乎其微的耗能将设置调的过低
其它:
- 队伍中必须包含**莉奈娅**,不建议在**挪德卡莱**地区携带**哥伦比娅**,部分矿点的犀牛会影响挖矿。
- 请配置好自动战斗策略,建议关闭战斗后的自动拾取,战斗点位仅为必经之路或高收益处。
- 若观察到部分点位射一箭无法满足挖掘全部,请反馈给我(如果漏的是铁矿不会考虑)
- 若观察到部分点位射一箭无法满足挖掘全部,请反馈给我(如果漏的是铁矿不会考虑,被小动物挡住也大概率不会修改
- 有一部分铁矿点位位置较为刁钻,加之铁矿这种矿物本身较无用,将不会收录在本脚本的路线内,同理,其它个别位置刁钻的矿物点位也不会专程前往。
- 可在设置中指定队伍名称。
## 未来计划
## 未来
- 运行指定时长
- 效率排序
> 注:未来的部分逻辑会在最大程度减少配置项的基础上开发,可能会参考[矿产资源批发](https://bgi.sh/?type=js&path=AbundantOre)。
> 注:未来的部分逻辑会在**最大程度减少配置项**的基础上开发,可能会参考[矿产资源批发](https://bgi.sh/?type=js&path=AbundantOre)。
> 考虑到运行时长,不会支持多账号
> 又注:当前脚本仅为先行版,作占位避免撞车,因此缺少大部分路线,且路线暂未深度优化。所有国家都补全时将变更为正式版。

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,123 +1,175 @@
import { checkVersion } from "./utils/version.js";
import { checkAvatar } from "./utils/avatar.js";
import { getRoutes, filterByTags, filterByRegion } from "./utils/routes.js";
import { loadRefreshData, cleanupStaleRecords, recordRoute, filterRunnableRoutes, getRouteDuration, estimateRoutesDuration, formatDuration } from "./utils/refresh.js";
import { checkVersion } from "./utils/version.js"
import { checkAvatar } from "./utils/avatar.js"
import {
getRoutes,
filterByTags,
filterByRegion
} from "./utils/routes.js"
import {
loadRefreshData,
cleanupStaleRecords,
recordRoute,
filterRunnableRoutes,
getRouteDuration,
estimateRoutesDuration,
formatDuration
} from "./utils/refresh.js"
import {
parseRunTimeLimit,
isTimeUp
} from "./utils/timeControl.js"
// import {
// getInventory,
// formatYieldDiff,
// formatInventory
// } from "./utils/inventory.js"
import {startMonthCardWatcher} from "../../../packages/utils/tool"
// 切换队伍
async function switchParty(partyName) {
if (!partyName) {
return
}
try {
partyName = partyName.trim()
log.info("切换队伍: " + partyName)
if (!await genshin.switchParty(partyName)) {
log.info("切换失败,前往七天神像重试")
await genshin.tpToStatueOfTheSeven()
await genshin.switchParty(partyName)
}
} catch {
log.error("队伍切换失败")
await genshin.returnMainUi()
}
}
// 运行单条路线
async function runRoute(routePath) {
try {
await pathingScript.runFile(routePath)
return true
} catch (err) {
log.error(`路线运行失败 ${routePath}:`, err)
return false
}
}
(async function () {
// 开启月卡监听
const watcher = startMonthCardWatcher()
const version = getVersion()
const minVersion = '0.60.2-alpha.3'
const minVersion = '0.60.2-alpha.5'
if (!checkVersion(version, minVersion)) {
log.warn(`当前 BetterGI 版本(${version})低于最低要求(${minVersion})内部算法缺少优化,出现异常为正常情况`)
log.warn(`当前 BetterGI 版本(${version})低于最低要求(${minVersion}),出现异常为正常情况`)
}
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
setGameMetrics(1920, 1080, 1)
await genshin.returnMainUi()
const partyName = settings.partyName || "";
const excludeOreTypes = Array.from(settings.excludeOreTypes || []);
const excludeRegions = Array.from(settings.excludeRegions || []);
const skipBattleRoutes = settings.skipBattleRoutes === true;
const partyName = settings.partyName || ""
const excludeOreTypes = Array.from(settings.excludeOreTypes || [])
const excludeRegions = Array.from(settings.excludeRegions || [])
const skipBattleRoutes = settings.skipBattleRoutes === true
const targetRunningMinutes = settings.targetRunningMinutes || null
// 切换队伍
async function switchParty(partyName) {
if (!partyName) {
return;
}
try {
partyName = partyName.trim();
log.info("切换队伍: " + partyName);
if (!await genshin.switchParty(partyName)) {
log.info("切换失败,前往七天神像重试");
await genshin.tpToStatueOfTheSeven();
await genshin.switchParty(partyName);
}
} catch {
log.error("队伍切换失败");
notification.error(`队伍切换失败`);
await genshin.returnMainUi();
}
}
// 运行单条路线
async function runRoute(routePath) {
try {
await pathingScript.runFile(routePath);
} catch (err) {
log.error(`路线运行失败 ${routePath}:`, err);
}
}
// 主逻辑
await switchParty(partyName);
if (!checkAvatar("莉奈娅")) {
log.error("队伍角色检查失败 - 未找到莉奈娅,脚本终止");
return;
}
const allRoutes = getRoutes();
const allRoutes = getRoutes()
if (allRoutes.length === 0) {
log.error("未找到任何路线文件请确保paths目录下存在路线文件。");
return;
log.error("未找到任何路线文件请确保paths目录下存在路线文件。")
return
}
let routes = allRoutes;
let routes = allRoutes
routes = filterByRegion(routes, excludeRegions);
routes = filterByRegion(routes, excludeRegions)
if (excludeRegions.length > 0 && routes.length < allRoutes.length) {
log.info(`地区筛选:排除 ${allRoutes.length - routes.length} 条路线`);
log.info(`地区筛选:排除 ${allRoutes.length - routes.length} 条路线`)
}
routes = filterByTags(routes, excludeOreTypes);
routes = filterByTags(routes, excludeOreTypes)
if (excludeOreTypes.length > 0 && routes.length < allRoutes.length) {
log.info(`矿物筛选:排除 ${allRoutes.length - routes.length} 条路线`);
log.info(`矿物筛选:排除 ${allRoutes.length - routes.length} 条路线`)
}
if (skipBattleRoutes) {
routes = filterByTags(routes, ["战斗"]);
routes = filterByTags(routes, ["战斗"])
if (routes.length < allRoutes.length) {
log.info(`跳过战斗路线:排除 ${allRoutes.length - routes.length} 条路线`);
log.info(`跳过战斗路线:排除 ${allRoutes.length - routes.length} 条路线`)
}
}
const refreshData = loadRefreshData();
cleanupStaleRecords(refreshData, routes);
const refreshData = loadRefreshData()
cleanupStaleRecords(refreshData, routes)
const runnableRoutes = filterRunnableRoutes(routes, refreshData);
const runnableRoutes = filterRunnableRoutes(routes, refreshData)
if (runnableRoutes.length === 0) {
log.info("所有路线均未刷新,无需运行");
return;
log.info("所有路线均未刷新,无需运行")
return
}
log.info(`将运行 ${runnableRoutes.length}/${allRoutes.length} 条路线`);
await switchParty(partyName)
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
if (!checkAvatar("莉奈娅")) {
log.error("队伍角色检查失败 - 未找到莉奈娅,脚本终止")
return
}
log.info(`将运行 ${runnableRoutes.length}/${allRoutes.length} 条路线`)
const runUntilTime = parseRunTimeLimit(targetRunningMinutes)
if (runUntilTime) {
const minutes = Math.round((runUntilTime - Date.now()) / 60 / 1000)
log.info(`将在 ${minutes} 分钟后停止运行`)
}
dispatcher.addTimer(new RealtimeTimer("AutoPick"))
// const originalInventory = await getInventory()
// log.info("当前背包:" + formatInventory(originalInventory))
const scriptStartTime = Date.now()
for (let i = 0; i < runnableRoutes.length; i++) {
const route = runnableRoutes[i];
const fileName = route.split('\\').pop();
if (isTimeUp(runUntilTime)) {
log.info("已到达运行时长限制,停止运行")
break
}
const route = runnableRoutes[i]
const fileName = route.split('\\').pop()
const remaining = runnableRoutes.slice(i);
const remainingEst = estimateRoutesDuration(remaining, refreshData);
const thisRouteEst = getRouteDuration(route, refreshData);
const remaining = runnableRoutes.slice(i)
const remainingEst = estimateRoutesDuration(remaining, refreshData)
const thisRouteEst = getRouteDuration(route, refreshData)
if (thisRouteEst !== null) {
log.info(`路线 ${i + 1}/${runnableRoutes.length}: ${fileName}(预计 ${formatDuration(thisRouteEst)},剩余 ${formatDuration(remainingEst)}`);
log.info(`路线 ${i + 1}/${runnableRoutes.length}: ${fileName}(预计需要时间: ${formatDuration(thisRouteEst)},剩余时间: ${formatDuration(remainingEst)}`)
} else {
log.info(`路线 ${i + 1}/${runnableRoutes.length}: ${fileName}(预计 ${formatDuration(remainingEst)}`);
log.info(`路线 ${i + 1}/${runnableRoutes.length}: ${fileName}(预计剩余时间: ${formatDuration(remainingEst)}`)
}
const startTime = Date.now();
await runRoute(route);
await sleep(10);
const duration = (Date.now() - startTime) / 1000;
recordRoute(route, refreshData, duration);
const startTime = Date.now()
await sleep(10)
const res = await runRoute(route)
if (res) {
const duration = (Date.now() - startTime) / 1000
recordRoute(route, refreshData, duration)
}
if (i < runnableRoutes.length - 1) {
await sleep(2000);
// 最后一条路线,给一定时间缓冲
if (i === runnableRoutes.length - 1) {
await sleep(2000)
}
}
log.info("所有路线运行完成");
})();
log.info("所有路线运行完成")
// const latestInventory = await getInventory()
// const runningMinutes = (Date.now() - scriptStartTime) / 1000 / 60
// const summary = `运行${runningMinutes.toFixed(2)}分钟,${formatYieldDiff(latestInventory, originalInventory)}`
// log.info("当前背包:" + formatInventory(latestInventory))
// log.info(summary)
await watcher.cancel()
})()

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778073758649,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A01-希汐岛左下-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778073869657,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A01-希汐岛左下-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074015548,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A01-希汐岛左下-03",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074022774,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A02-沐光之台",
"tags": [

View File

@@ -9,8 +9,8 @@
"bgi_version": "0.60.1",
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074188795,
"map_match_method": "",
"last_modified_time": 1778390462884,
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A03-月矩力实验设计局左侧",
"tags": [
@@ -135,8 +135,8 @@
"id": 13,
"move_mode": "walk",
"type": "path",
"x": 10095.1797,
"y": 3334.8125
"x": 10099.8203,
"y": 3334.2969
},
{
"action": "",

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074354807,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A04-月矩力实验设计局右侧",
"tags": [

View File

@@ -9,8 +9,8 @@
"bgi_version": "0.60.1",
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074685948,
"map_match_method": "",
"last_modified_time": 1778390516494,
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A05-刻拉蒂之眼",
"tags": [
@@ -107,8 +107,8 @@
"id": 10,
"move_mode": "walk",
"type": "path",
"x": 8657.875,
"y": 2271.8516
"x": 8658.7773,
"y": 2271.6797
},
{
"action": "",
@@ -116,31 +116,13 @@
"id": 11,
"move_mode": "walk",
"type": "path",
"x": 8657.9258,
"y": 2266.1318
},
{
"action": "linnea_mining",
"action_params": "",
"id": 12,
"move_mode": "walk",
"type": "orientation",
"x": 8664.4902,
"y": 2264.5488
},
{
"action": "",
"action_params": "",
"id": 13,
"move_mode": "walk",
"type": "path",
"x": 8663.6855,
"y": 2286.3545
},
{
"action": "linnea_mining",
"action_params": "",
"id": 14,
"id": 12,
"move_mode": "walk",
"type": "orientation",
"x": 8656.8262,
@@ -149,7 +131,7 @@
{
"action": "",
"action_params": "",
"id": 15,
"id": 13,
"move_mode": "walk",
"type": "path",
"x": 8637.0225,
@@ -158,7 +140,7 @@
{
"action": "",
"action_params": "",
"id": 16,
"id": 14,
"move_mode": "walk",
"type": "path",
"x": 8643.2637,
@@ -167,7 +149,7 @@
{
"action": "linnea_mining",
"action_params": "",
"id": 17,
"id": 15,
"move_mode": "walk",
"type": "orientation",
"x": 8652.0293,
@@ -176,7 +158,7 @@
{
"action": "linnea_mining",
"action_params": "",
"id": 18,
"id": 16,
"move_mode": "walk",
"type": "orientation",
"x": 8644.9844,
@@ -185,7 +167,7 @@
{
"action": "",
"action_params": "",
"id": 19,
"id": 17,
"move_mode": "walk",
"type": "path",
"x": 8646.9629,
@@ -194,7 +176,7 @@
{
"action": "",
"action_params": "",
"id": 20,
"id": 18,
"move_mode": "walk",
"type": "path",
"x": 8669.3848,
@@ -203,7 +185,7 @@
{
"action": "",
"action_params": "",
"id": 21,
"id": 19,
"move_mode": "walk",
"type": "path",
"x": 8675.917,
@@ -212,7 +194,7 @@
{
"action": "linnea_mining",
"action_params": "",
"id": 22,
"id": 20,
"move_mode": "walk",
"type": "orientation",
"x": 8682.9688,
@@ -221,7 +203,7 @@
{
"action": "",
"action_params": "",
"id": 23,
"id": 21,
"move_mode": "walk",
"type": "path",
"x": 8714.1064,
@@ -230,7 +212,7 @@
{
"action": "mining",
"action_params": "",
"id": 24,
"id": 22,
"move_mode": "walk",
"type": "path",
"x": 8715.5771,
@@ -239,7 +221,7 @@
{
"action": "",
"action_params": "",
"id": 25,
"id": 23,
"move_mode": "walk",
"type": "path",
"x": 8728.0361,
@@ -248,7 +230,7 @@
{
"action": "mining",
"action_params": "",
"id": 26,
"id": 24,
"move_mode": "walk",
"type": "target",
"x": 8726.9141,
@@ -257,7 +239,7 @@
{
"action": "mining",
"action_params": "",
"id": 27,
"id": 25,
"move_mode": "walk",
"type": "target",
"x": 8731.1055,
@@ -266,7 +248,7 @@
{
"action": "",
"action_params": "",
"id": 28,
"id": 26,
"move_mode": "walk",
"type": "path",
"x": 8733.9531,
@@ -275,7 +257,7 @@
{
"action": "linnea_mining",
"action_params": "",
"id": 29,
"id": 27,
"move_mode": "walk",
"type": "orientation",
"x": 8749.1123,
@@ -284,7 +266,7 @@
{
"action": "",
"action_params": "",
"id": 30,
"id": 28,
"move_mode": "walk",
"type": "path",
"x": 8735.2188,
@@ -293,7 +275,7 @@
{
"action": "",
"action_params": "",
"id": 31,
"id": 29,
"move_mode": "walk",
"type": "path",
"x": 8710.1875,
@@ -302,7 +284,7 @@
{
"action": "linnea_mining",
"action_params": "",
"id": 32,
"id": 30,
"move_mode": "walk",
"type": "orientation",
"x": 8708.666,

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777628877185,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A06-蓝珀胡左上",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074768168,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A07-苔骨荒原-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074799690,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A07-苔骨荒原-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074828909,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A07-苔骨荒原-03",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074887381,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A08-星砂滩左侧-01",
"tags": [

View File

@@ -9,8 +9,8 @@
"bgi_version": "0.60.1",
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777641918526,
"map_match_method": "",
"last_modified_time": 1778390547978,
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A08-星砂滩左侧-02",
"tags": [
@@ -58,7 +58,7 @@
},
{
"action": "linnea_mining",
"action_params": "2,4",
"action_params": "2,5",
"id": 5,
"move_mode": "walk",
"type": "orientation",

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778074909070,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A08-星砂滩左侧-03",
"tags": [

View File

@@ -9,8 +9,8 @@
"bgi_version": "0.60.1",
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629033680,
"map_match_method": "",
"last_modified_time": 1778390565171,
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A09-那夏镇下方",
"tags": [
@@ -67,7 +67,7 @@
},
{
"action": "linnea_mining",
"action_params": "2,3",
"action_params": "2,4",
"id": 6,
"move_mode": "walk",
"type": "orientation",

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778075130600,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A10-那夏镇左侧",
"tags": [

View File

@@ -9,8 +9,8 @@
"bgi_version": "0.60.1",
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778075142681,
"map_match_method": "",
"last_modified_time": 1778390589087,
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A11-叮铃哐啷蛋卷工坊",
"tags": [
@@ -76,7 +76,7 @@
},
{
"action": "linnea_mining",
"action_params": "",
"action_params": "2",
"id": 7,
"move_mode": "walk",
"type": "orientation",
@@ -103,7 +103,7 @@
},
{
"action": "linnea_mining",
"action_params": "",
"action_params": "2,3",
"id": 10,
"move_mode": "walk",
"type": "orientation",

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1778075169912,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A12-苔古荒原地下",
"tags": [

View File

@@ -0,0 +1,234 @@
{
"config": {
"realtime_triggers": {
"AutoPick": true
}
},
"farming_info": {
"allow_farming_count": false,
"duration_seconds": 0,
"elite_details": "",
"elite_mob_count": 0,
"normal_mob_count": 0,
"primary_target": "",
"total_mora": 0
},
"info": {
"authors": [
{
"links": "https://github.com/zaodonganqi",
"name": "躁动的氨气"
}
],
"bgi_version": "0.60.2-alpha.4",
"description": "",
"enable_monster_loot_split": false,
"items": [],
"last_modified_time": 1778390948061,
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "A16-星砂滩右侧",
"order": 0,
"tags": [
"虹滴晶"
],
"type": "collect",
"version": ""
},
"positions": [
{
"action": "",
"id": 1,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "teleport",
"x": 9563.1123,
"y": 2135.2529
},
{
"action": "",
"id": 2,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9536.6689,
"y": 2131.8125
},
{
"action": "stop_flying",
"action_params": "",
"id": 3,
"move_mode": "fly",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9521.4668,
"y": 2130.478
},
{
"action": "",
"id": 4,
"move_mode": "dash",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9427.0605,
"y": 2134.2529
},
{
"action": "",
"id": 5,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9397.4619,
"y": 2138.647
},
{
"action": "",
"id": 6,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9388.877,
"y": 2140.0547
},
{
"action": "",
"id": 7,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9368.5508,
"y": 2155.9229
},
{
"action": "combat_script",
"action_params": "wait(4)",
"id": 8,
"locked": false,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9360.0947,
"y": 2159.4561
},
{
"action": "",
"id": 9,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "path",
"x": 9360.0947,
"y": 2159.4561
},
{
"action": "linnea_mining",
"action_params": "3,6",
"id": 10,
"move_mode": "walk",
"point_ext_params": {
"description": "",
"enable_monster_loot_split": false,
"misidentification": {
"arrival_time": 0,
"handling_mode": "previousDetectedPoint",
"type": [
"unrecognized"
]
}
},
"type": "orientation",
"x": 9356.416,
"y": 2156.167
}
]
}

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629515332,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629562187,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629583442,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-03",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629601206,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-04",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629625023,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-05",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629653673,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-06",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629664654,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B01-悠悠度假村-07",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629830922,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B02-安绕之野-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629847224,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B02-安绕之野-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629860170,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B02-安绕之野-03",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629927183,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B02-安绕之野-04",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629908885,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B02-安绕之野-05",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629963179,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B03-奥奇卡纳塔-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629979794,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B03-奥奇卡纳塔-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777629996165,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B03-奥奇卡纳塔-03",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630005091,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B03-奥奇卡纳塔-04",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630017342,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B03-奥奇卡纳塔-05",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630038028,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B04-花羽会上方-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630079731,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B04-花羽会上方-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630107242,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B04-花羽会上方-03",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630098385,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B04-花羽会上方-04",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630131255,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B05-花羽会下方-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630148532,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B05-花羽会下方-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630165708,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B06-烟谜主左上-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630181378,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B06-烟谜主左上-02",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630258862,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B07-烟谜主右下-01",
"tags": [

View File

@@ -10,7 +10,7 @@
"description": "",
"enable_monster_loot_split": false,
"last_modified_time": 1777630246744,
"map_match_method": "",
"map_match_method": "TemplateMatch",
"map_name": "Teyvat",
"name": "B07-烟谜主右下-02",
"tags": [

View File

@@ -1,13 +1,4 @@
[
{
"name": "partyName",
"type": "input-text",
"default": "",
"label": "队伍名称"
},
{
"type": "separator"
},
{
"name": "excludeRegions",
"type": "multi-checkbox",
@@ -23,7 +14,7 @@
{
"name": "excludeOreTypes",
"type": "multi-checkbox",
"label": "不挖以下矿物",
"label": "\n不挖以下矿物",
"options": [
"虹滴晶",
"白铁块",
@@ -39,6 +30,24 @@
"name": "skipBattleRoutes",
"type": "checkbox",
"default": false,
"label": "跳过战斗点位"
"label": "\n跳过战斗点位"
},
{
"type": "separator"
},
{
"name": "partyName",
"type": "input-text",
"default": "",
"label": "\n队伍名称"
},
{
"type": "separator"
},
{
"name": "targetRunningMinutes",
"type": "input-text",
"default": "",
"label": "\n目标运行时长分钟"
}
]

View File

@@ -1,14 +1,12 @@
此目录下的文件为工具脚本,与主脚本的运行无关,仅用于开发时批量处理路线信息。
所有命令均从项目根目录运行。
---
## split-routes.js
将包含多个传送点(`teleport`)的路线拆分为独立子路线,方便打 tag 管理。
```bash
node repo/js/LinneaMining/tools/split-routes.js
node split-routes.js
```
拆分后自动运行 `reindex-positions.js`
@@ -17,7 +15,7 @@ node repo/js/LinneaMining/tools/split-routes.js
将拆分后的子路线(`名称-01.json``名称-02.json`...重新合并为父路线tags 自动合并去重。
```bash
node repo/js/LinneaMining/tools/merge-routes.js
node merge-routes.js
```
合并后自动运行 `reindex-positions.js`
@@ -26,26 +24,26 @@ node repo/js/LinneaMining/tools/merge-routes.js
将所有路线文件中 positions 的 `id` 按数组顺序重新赋值1, 2, 3...),附带 BOM 清理。
```bash
node repo/js/LinneaMining/tools/reindex-positions.js
node reindex-positions.js
```
## update-bgi-version.js
批量更新路线文件中 `info.bgi_version`,补充缺失的 `info.version`,附带 BOM 清理。修改脚本中的 `newVersion` 常量后运行。
```bash
node repo/js/LinneaMining/tools/update-bgi-version.js
node update-bgi-version.js
```
## rename-tag.js
将路线文件中匹配指定 tag 替换为新 tag精确匹配单个元素。修改脚本中的 `oldTag``newTag` 后运行。
```bash
node repo/js/LinneaMining/tools/rename-tag.js
node rename-tag.js
```
## cleanup-fields.js
批量删除路线文件中的 `config``farming_info` 字段,附带 BOM 清理。
```bash
node repo/js/LinneaMining/tools/cleanup-fields.js
node cleanup-fields.js
```

View File

@@ -5,12 +5,12 @@
* @returns {boolean} 队伍中是否包含该角色
*/
function checkAvatar(targetName) {
const avatars = getAvatars();
if (!avatars || avatars.length < 1) return false;
const avatars = getAvatars()
if (!avatars || avatars.length < 1) return false
for (let i = 0; i < avatars.length; i++) {
if (avatars[i] === targetName) return true;
if (avatars[i] === targetName) return true
}
return false;
return false
}
export { checkAvatar };
export { checkAvatar }

View File

@@ -0,0 +1,112 @@
import {openBag} from "../../../../packages/utils/tool"
import crystal_chunk from "../assets/images/crystal_chunk.png"
import amethyst_lump from "../assets/images/amethyst_lump.png"
import condessence_crystal from "../assets/images/condessence_crystal.png"
import rainbowdrop_crystal from "../assets/images/rainbowdrop_crystal.png"
import white_iron_chunk from "../assets/images/white_iron_chunk.png"
import iron_chunk from "../assets/images/iron_chunk.png"
const ORES = [
{ name: "水晶块", mat: crystal_chunk },
{ name: "紫晶块", mat: amethyst_lump },
{ name: "萃凝晶", mat: condessence_crystal },
{ name: "虹滴晶", mat: rainbowdrop_crystal },
{ name: "白铁块", mat: white_iron_chunk },
{ name: "铁块", mat: iron_chunk },
]
/**
* 检测背包中各类矿石的数量
* 流程: 打开背包 -> 切换素材页 -> 模板匹配矿石图标 -> OCR 读取数量
*
* @returns {Object} 各矿石数量,键为矿石中文名,无法识别的数量为 0
*/
async function getInventory() {
await genshin.returnMainUi()
await openBag()
click(964, 53)
await sleep(500)
const result = Object.fromEntries(ORES.map(o => [o.name, 0]))
const gameRegion = captureGameRegion()
for (const ore of ORES) {
const ro = RecognitionObject.TemplateMatch(ore.mat)
ro.threshold = 0.8
ro.UseMask = true
const res = gameRegion.find(ro)
if (!res.isEmpty()) {
log.debug(`Found ${ore.name} at (${res.x}, ${res.y})`)
const ocrRes = gameRegion.find(
RecognitionObject.ocr(res.x, res.y + 120, 120, 40)
)
if (ocrRes) {
if (!isNaN(count) && count >= 0) {
result[ore.name] = count
} else {
log.warn(`OCR 识别矿石数量失败: ${ore.name}, 文本: ${ocrRes.text}`)
}
}
}
}
gameRegion.dispose()
await genshin.returnMainUi()
return result
}
/**
* 计算两次背包检测结果之间的矿石总增量
*
* @param {Object} current - 当前检测结果
* @param {Object} previous - 之前检测结果
* @returns {number} 总增量
*/
function calcYield(current, previous) {
let total = 0
for (const key of Object.keys(current)) {
total += (current[key] || 0) - (previous[key] || 0)
}
return total
}
/**
* 格式化矿石变化量为可读字符串
* 仅输出有变化的矿石,无变化时返回 "无收获"
*
* @param {Object} current - 当前检测结果
* @param {Object} previous - 之前检测结果
* @returns {string} 例如 "水晶块+5萃凝晶+3"
*/
function formatYieldDiff(current, previous) {
const parts = []
for (const ore of ORES) {
const diff = (current[ore.name] || 0) - (previous[ore.name] || 0)
if (diff !== 0) {
parts.push(`${ore.name}${diff > 0 ? "+" : ""}${diff}`)
}
}
return parts.length > 0 ? parts.join("") : "无收获"
}
/**
* 格式化矿石数量为日志字符串
*
* @param {Object} inventory - 检测结果,键为矿石中文名
* @returns {string} 例如 "水晶块10个紫晶块5个"
*/
function formatInventory(inventory) {
return ORES
.filter(o => inventory[o.name] !== undefined)
.map(o => `${o.name}${inventory[o.name]}`)
.join("")
}
export {
getInventory,
calcYield,
formatYieldDiff,
formatInventory
}

View File

@@ -1,5 +1,5 @@
const REFRESH_DATA_PATH = "local/refresh_records.json";
const FALLBACK_DURATION = 60;
const REFRESH_DATA_PATH = "local/refresh_records.json"
const FALLBACK_DURATION = 60
/**
* 从本地文件加载刷新记录数据
@@ -8,10 +8,10 @@ const FALLBACK_DURATION = 60;
*/
function loadRefreshData() {
try {
const raw = file.readTextSync(REFRESH_DATA_PATH);
return JSON.parse(raw) || {};
const raw = file.readTextSync(REFRESH_DATA_PATH)
return JSON.parse(raw) || {}
} catch {
return {};
return {}
}
}
@@ -21,7 +21,7 @@ function loadRefreshData() {
* @param {Object} data - 刷新记录对象
*/
function saveRefreshData(data) {
file.writeTextSync(REFRESH_DATA_PATH, JSON.stringify(data, null, 2));
file.writeTextSync(REFRESH_DATA_PATH, JSON.stringify(data, null, 2))
}
/**
@@ -32,20 +32,20 @@ function saveRefreshData(data) {
* @returns {Object} 清理后的刷新记录对象
*/
function cleanupStaleRecords(data, routePaths) {
const pathSet = new Set(routePaths);
const keys = Object.keys(data);
let removed = 0;
const pathSet = new Set(routePaths)
const keys = Object.keys(data)
let removed = 0
for (const key of keys) {
if (!pathSet.has(key)) {
delete data[key];
removed++;
delete data[key]
removed++
}
}
if (removed > 0) {
log.info(`清理了 ${removed} 条过期路线记录`);
saveRefreshData(data);
log.info(`清理了 ${removed} 条过期路线记录`)
saveRefreshData(data)
}
return data;
return data
}
/**
@@ -57,14 +57,14 @@ function cleanupStaleRecords(data, routePaths) {
* @param {number} duration - 本次运行时长0 或负数时不记录时长
*/
function recordRoute(routePath, data, duration) {
const existing = data[routePath];
const existing = data[routePath]
if (existing && typeof existing === 'object') {
existing.t = Date.now();
if (duration > 0) existing.d = Math.round(duration);
existing.t = Date.now()
if (duration > 0) existing.d = Math.round(duration)
} else {
data[routePath] = { t: Date.now(), d: duration > 0 ? Math.round(duration) : null };
data[routePath] = { t: Date.now(), d: duration > 0 ? Math.round(duration) : null }
}
saveRefreshData(data);
saveRefreshData(data)
}
/**
@@ -77,17 +77,17 @@ function recordRoute(routePath, data, duration) {
* @returns {boolean} 矿石是否已刷新,记录不存在视为已刷新
*/
function isRouteReady(routePath, data, refreshDays = 3) {
const record = data[routePath];
const lastRun = record?.t || (typeof record === 'number' ? record : null);
if (!lastRun) return true;
const record = data[routePath]
const lastRun = record?.t || (typeof record === 'number' ? record : null)
if (!lastRun) return true
const t = lastRun / 1000;
let t0 = Math.floor(t / 86400) * 86400 + 57600;
const t = lastRun / 1000
let t0 = Math.floor(t / 86400) * 86400 + 57600
if (t0 > t) {
t0 -= 86400;
t0 -= 86400
}
const respawnTime = t0 + 86400 * refreshDays;
return respawnTime < Date.now() / 1000;
const respawnTime = t0 + 86400 * refreshDays
return respawnTime < Date.now() / 1000
}
/**
@@ -99,16 +99,16 @@ function isRouteReady(routePath, data, refreshDays = 3) {
* @returns {string[]} 已刷新的路线文件路径数组
*/
function filterRunnableRoutes(routePaths, data, refreshDays = 3) {
const runnable = [];
const runnable = []
for (const routePath of routePaths) {
if (isRouteReady(routePath, data, refreshDays)) {
runnable.push(routePath);
runnable.push(routePath)
} else {
const fileName = routePath.split('\\').pop();
log.info(`跳过未刷新路线: ${fileName}`);
const fileName = routePath.split('\\').pop()
log.info(`跳过未刷新路线: ${fileName}`)
}
}
return runnable;
return runnable
}
/**
@@ -119,10 +119,10 @@ function filterRunnableRoutes(routePaths, data, refreshDays = 3) {
* @returns {number|null} 历史运行时长(秒),无记录返回 null
*/
function getRouteDuration(routePath, data) {
const record = data[routePath];
if (!record) return null;
if (typeof record === 'object' && record.d) return record.d;
return null;
const record = data[routePath]
if (!record) return null
if (typeof record === 'object' && record.d) return record.d
return null
}
/**
@@ -134,12 +134,12 @@ function getRouteDuration(routePath, data) {
* @returns {number} 估算总时长(秒)
*/
function estimateRoutesDuration(routePaths, data) {
let total = 0;
let total = 0
for (const routePath of routePaths) {
const d = getRouteDuration(routePath, data);
total += d !== null ? d : FALLBACK_DURATION;
const d = getRouteDuration(routePath, data)
total += d !== null ? d : FALLBACK_DURATION
}
return total;
return total
}
/**
@@ -150,14 +150,24 @@ function estimateRoutesDuration(routePaths, data) {
* @returns {string} 格式化后的时长字符串
*/
function formatDuration(seconds) {
seconds = Math.max(0, Math.round(seconds));
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const mm = String(m).padStart(2, '0');
const ss = String(s).padStart(2, '0');
if (h > 0) return `${h}小时${mm}${ss}`;
return `${mm}${ss}`;
seconds = Math.max(0, Math.round(seconds))
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = seconds % 60
const mm = String(m).padStart(2, '0')
const ss = String(s).padStart(2, '0')
if (h > 0) return `${h}小时${mm}${ss}`
return `${mm}${ss}`
}
export { loadRefreshData, saveRefreshData, cleanupStaleRecords, recordRoute, isRouteReady, filterRunnableRoutes, getRouteDuration, estimateRoutesDuration, formatDuration };
export {
loadRefreshData,
saveRefreshData,
cleanupStaleRecords,
recordRoute,
isRouteReady,
filterRunnableRoutes,
getRouteDuration,
estimateRoutesDuration,
formatDuration
}

View File

@@ -5,18 +5,18 @@
* @returns {string[]} 所有 JSON 文件的完整路径数组
*/
function readRouteFiles(folderPath) {
const files = [];
const entries = file.ReadPathSync(folderPath);
const files = []
const entries = file.ReadPathSync(folderPath)
for (const entry of entries) {
if (file.IsFolder(entry)) {
files.push(...readRouteFiles(entry));
files.push(...readRouteFiles(entry))
} else if (entry.endsWith(".json")) {
files.push(entry);
files.push(entry)
}
}
return files;
return files
}
/**
@@ -26,10 +26,10 @@ function readRouteFiles(folderPath) {
*/
function getRoutes() {
try {
return readRouteFiles("paths");
return readRouteFiles("paths")
} catch (err) {
log.error("获取路线文件时出错:", err);
return [];
log.error("获取路线文件时出错:", err)
return []
}
}
@@ -41,19 +41,19 @@ function getRoutes() {
* @returns {string[]} 过滤后的路线文件路径数组
*/
function filterByTags(routePaths, excludedTags) {
if (!excludedTags || excludedTags.length === 0) return routePaths;
if (!excludedTags || excludedTags.length === 0) return routePaths
return routePaths.filter(routePath => {
try {
const raw = file.readTextSync(routePath);
const data = JSON.parse(raw);
const tags = data.info?.tags;
if (!tags || tags.length === 0) return true;
return tags.some(tag => !excludedTags.includes(tag));
const raw = file.readTextSync(routePath)
const data = JSON.parse(raw)
const tags = data.info?.tags
if (!tags || tags.length === 0) return true
return tags.some(tag => !excludedTags.includes(tag))
} catch {
return true;
return true
}
});
})
}
/**
@@ -64,14 +64,18 @@ function filterByTags(routePaths, excludedTags) {
* @returns {string[]} 过滤后的路线文件路径数组
*/
function filterByRegion(routePaths, excludedRegions) {
if (!excludedRegions || excludedRegions.length === 0) return routePaths;
if (!excludedRegions || excludedRegions.length === 0) return routePaths
return routePaths.filter(routePath => {
const parts = routePath.replace(/\\/g, '/').split('/');
const regionIdx = parts.indexOf("paths") + 1;
if (regionIdx >= parts.length) return true;
return !excludedRegions.includes(parts[regionIdx]);
});
const parts = routePath.replace(/\\/g, '/').split('/')
const regionIdx = parts.indexOf("paths") + 1
if (regionIdx >= parts.length) return true
return !excludedRegions.includes(parts[regionIdx])
})
}
export { getRoutes, filterByTags, filterByRegion };
export {
getRoutes,
filterByTags,
filterByRegion
}

View File

@@ -0,0 +1,30 @@
/**
* 根据目标运行分钟数计算截止时间戳
*
* @param {number|string|null} targetMinutes - 目标运行分钟数
* @returns {number|null} 截止时间戳,未设置返回 null
*/
function parseRunTimeLimit(targetMinutes) {
if (!targetMinutes) return null
const minutes = Number(targetMinutes)
if (isNaN(minutes) || minutes <= 0) {
log.warn(`无效的运行时长设置: ${targetMinutes}`)
return null
}
return Date.now() + minutes * 60 * 1000
}
/**
* 检查是否已到达运行截止时间
*
* @param {number|null} runUntilTime - 截止时间戳
* @returns {boolean}
*/
function isTimeUp(runUntilTime) {
return runUntilTime !== null && Date.now() >= runUntilTime
}
export {
parseRunTimeLimit,
isTimeUp
}

View File

@@ -26,4 +26,4 @@ function checkVersion(version, minVersion) {
return a >= b
}
export { checkVersion };
export { checkVersion }