427 lines
18 KiB
Python
427 lines
18 KiB
Python
import json
|
|
from datetime import datetime, timedelta
|
|
|
|
from evalidate import Expr
|
|
|
|
from mower.solvers.base_schedule import BaseSchedulerSolver
|
|
from mower.solvers.depot_reader import DepotManager
|
|
from mower.solvers.reclamation_algorithm import ReclamationAlgorithm
|
|
from mower.solvers.secret_front import SecretFront
|
|
from mower.utils import config, path
|
|
from mower.utils.csleep import MowerExit
|
|
from mower.utils.datetime import format_time
|
|
from mower.utils.device.adb_client.session import Session
|
|
from mower.utils.device.scrcpy import Scrcpy
|
|
from mower.utils.email import send_message, task_template
|
|
from mower.utils.log import logger
|
|
from mower.utils.logic_expression import get_logic_exp
|
|
from mower.utils.path import get_path
|
|
from mower.utils.plan import Plan, PlanConfig, Room
|
|
from mower.utils.simulator import restart_simulator
|
|
|
|
base_scheduler = None
|
|
operators = config.operators
|
|
|
|
|
|
# 执行自动排班
|
|
def main():
|
|
logger.info("开始运行Mower")
|
|
simulate()
|
|
|
|
|
|
def initialize(
|
|
tasks: list, scheduler: BaseSchedulerSolver | None = None
|
|
) -> BaseSchedulerSolver:
|
|
if scheduler:
|
|
scheduler.handle_error(True)
|
|
return scheduler
|
|
|
|
base_scheduler = BaseSchedulerSolver()
|
|
plan1 = {}
|
|
plan = config.plan.model_dump(exclude_none=True)
|
|
conf = config.conf
|
|
plan_config = PlanConfig(
|
|
rest_in_full=config.plan.conf.rest_in_full,
|
|
exhaust_require=config.plan.conf.exhaust_require,
|
|
resting_priority=config.plan.conf.resting_priority,
|
|
ling_xi=config.plan.conf.ling_xi,
|
|
workaholic=config.plan.conf.workaholic,
|
|
max_resting_count=config.plan.conf.max_resting_count,
|
|
free_blacklist=conf.free_blacklist,
|
|
resting_threshold=conf.resting_threshold,
|
|
refresh_trading_config=config.plan.conf.refresh_trading,
|
|
free_room=conf.free_room,
|
|
)
|
|
for room, obj in plan[plan["default"]].items():
|
|
plan1[room] = [
|
|
Room(op["agent"], op["group"], op["replacement"]) for op in obj["plans"]
|
|
]
|
|
# 默认任务
|
|
plan["default_plan"] = Plan(plan1, plan_config)
|
|
# 备用自定义任务
|
|
backup_plans: list[Plan] = []
|
|
|
|
for i in plan["backup_plans"]:
|
|
backup_plan: dict[str, Room] = {}
|
|
for room, obj in i["plan"].items():
|
|
backup_plan[room] = [
|
|
Room(op["agent"], op["group"], op["replacement"]) for op in obj["plans"]
|
|
]
|
|
backup_config = PlanConfig(
|
|
i["conf"]["rest_in_full"],
|
|
i["conf"]["exhaust_require"],
|
|
i["conf"]["resting_priority"],
|
|
ling_xi=i["conf"]["ling_xi"],
|
|
workaholic=i["conf"]["workaholic"],
|
|
max_resting_count=i["conf"]["max_resting_count"],
|
|
free_blacklist=i["conf"]["free_blacklist"],
|
|
resting_threshold=conf.resting_threshold,
|
|
refresh_trading_config=i["conf"]["refresh_trading"],
|
|
free_room=conf.free_room,
|
|
)
|
|
backup_trigger = get_logic_exp(i["trigger"]) if "trigger" in i else None
|
|
backup_task = i.get("task")
|
|
backup_trigger_timing = i.get("trigger_timing")
|
|
backup_plans.append(
|
|
Plan(
|
|
backup_plan,
|
|
backup_config,
|
|
trigger=backup_trigger,
|
|
task=backup_task,
|
|
trigger_timing=backup_trigger_timing,
|
|
)
|
|
)
|
|
plan["backup_plans"] = backup_plans
|
|
|
|
logger.debug(plan)
|
|
base_scheduler.global_plan = plan
|
|
base_scheduler.tasks = tasks
|
|
base_scheduler.enable_party = conf.enable_party == 1 # 是否使用线索
|
|
base_scheduler.leifeng_mode = conf.leifeng_mode == 1 # 是否有额外线索就送出
|
|
# 干员宿舍回复阈值
|
|
# 高效组心情低于 UpperLimit * 阈值 (向下取整)的时候才会会安排休息
|
|
base_scheduler.last_room = ""
|
|
# logger.info("宿舍黑名单:" + str(plan_config.free_blacklist))
|
|
# 估计没用了
|
|
base_scheduler.MAA = None
|
|
base_scheduler.error = False
|
|
base_scheduler.drone_room = None if conf.drone_room == "" else conf.drone_room
|
|
base_scheduler.reload_room = list(
|
|
filter(None, conf.reload_room.replace(",", ",").split(","))
|
|
)
|
|
|
|
# 关闭游戏次数计数器
|
|
base_scheduler.task_count = 0
|
|
|
|
return base_scheduler
|
|
|
|
|
|
def simulate():
|
|
"""
|
|
具体调用方法可见各个函数的参数说明
|
|
"""
|
|
logger.info(f"正在使用全局配置空间: {path.global_space}")
|
|
tasks = []
|
|
reconnect_max_tries = 10
|
|
reconnect_tries = 0
|
|
global base_scheduler
|
|
success = False
|
|
while not success:
|
|
try:
|
|
base_scheduler = initialize(tasks)
|
|
success = True
|
|
except MowerExit:
|
|
return
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
reconnect_tries += 1
|
|
if reconnect_tries < 3:
|
|
restart_simulator()
|
|
config.device.client.check_server_alive()
|
|
Session().connect()
|
|
if config.conf.droidcast.enable:
|
|
config.device.start_droidcast()
|
|
config.device.control.scrcpy = Scrcpy(config.device.client)
|
|
continue
|
|
else:
|
|
raise e
|
|
# base_scheduler.仓库扫描() #别删了 方便我找
|
|
validation_msg = base_scheduler.initialize_operators()
|
|
if validation_msg is not None:
|
|
logger.error(validation_msg)
|
|
return
|
|
if operators != {}:
|
|
for k, v in operators.items():
|
|
if (
|
|
k in base_scheduler.op_data.operators
|
|
and not base_scheduler.op_data.operators[k].room.startswith("dorm")
|
|
):
|
|
# 只复制心情数据
|
|
base_scheduler.op_data.operators[k].mood = v.mood
|
|
base_scheduler.op_data.operators[k].time_stamp = v.time_stamp
|
|
base_scheduler.op_data.operators[k].depletion_rate = v.depletion_rate
|
|
base_scheduler.op_data.operators[k].current_room = v.current_room
|
|
base_scheduler.op_data.operators[k].current_index = v.current_index
|
|
timezone_offset = 0
|
|
|
|
if len(base_scheduler.op_data.backup_plans) > 0:
|
|
conditions = base_scheduler.op_data.generate_conditions(
|
|
len(base_scheduler.op_data.backup_plans)
|
|
)
|
|
for con in conditions:
|
|
validation_msg = base_scheduler.op_data.swap_plan(con, True)
|
|
if validation_msg is not None:
|
|
logger.error(f"替换排班验证错误:{validation_msg}, 附表条件为 {con}")
|
|
return
|
|
base_scheduler.op_data.swap_plan(
|
|
[False] * len(base_scheduler.op_data.backup_plans), True
|
|
)
|
|
while True:
|
|
try:
|
|
if len(base_scheduler.tasks) > 0:
|
|
(base_scheduler.tasks.sort(key=lambda x: x.time, reverse=False))
|
|
logger.info("||".join([str(t) for t in base_scheduler.tasks]))
|
|
remaining_time = (
|
|
base_scheduler.tasks[0].time - datetime.now()
|
|
).total_seconds()
|
|
|
|
if remaining_time > 540:
|
|
# 刷新时间以鹰历为准
|
|
if (
|
|
base_scheduler.sign_in
|
|
< (datetime.now() - timedelta(hours=4)).date()
|
|
):
|
|
if base_scheduler.sign_in_plan_solver():
|
|
base_scheduler.sign_in = (
|
|
datetime.now() - timedelta(hours=4)
|
|
).date()
|
|
|
|
if (
|
|
base_scheduler.daily_visit_friend
|
|
< (datetime.now() - timedelta(hours=4)).date()
|
|
):
|
|
if base_scheduler.visit_friend_plan_solver():
|
|
base_scheduler.daily_visit_friend = (
|
|
datetime.now() - timedelta(hours=4)
|
|
).date()
|
|
|
|
if (
|
|
base_scheduler.daily_report
|
|
< (datetime.now() - timedelta(hours=4)).date()
|
|
):
|
|
if base_scheduler.report_plan_solver():
|
|
base_scheduler.daily_report = (
|
|
datetime.now() - timedelta(hours=4)
|
|
).date()
|
|
if (
|
|
base_scheduler.switch_assistants
|
|
< (datetime.now() - timedelta(hours=4)).date()
|
|
):
|
|
if base_scheduler.switch_assistants_plan_solver():
|
|
base_scheduler.switch_assistants = (
|
|
datetime.now() - timedelta(hours=4)
|
|
).date()
|
|
|
|
if (
|
|
config.conf.skland_enable
|
|
and base_scheduler.daily_skland
|
|
< (datetime.now() - timedelta(hours=4)).date()
|
|
):
|
|
if base_scheduler.skland_plan_solover():
|
|
base_scheduler.daily_skland = (
|
|
datetime.now() - timedelta(hours=4)
|
|
).date()
|
|
|
|
if (
|
|
config.conf.check_mail_enable
|
|
and base_scheduler.daily_mail
|
|
< (datetime.now() - timedelta(hours=8)).date()
|
|
):
|
|
if base_scheduler.mail_plan_solver():
|
|
base_scheduler.daily_mail = (
|
|
datetime.now() - timedelta(hours=8)
|
|
).date()
|
|
|
|
if config.conf.recruit_enable:
|
|
base_scheduler.recruit_plan_solver()
|
|
|
|
# 应该在maa任务之后
|
|
def _is_depotscan():
|
|
depot_manager = DepotManager()
|
|
return depot_manager.读取仓库()[-1]
|
|
|
|
if config.conf.maa_depot_enable:
|
|
dt = int(datetime.now().timestamp()) - _is_depotscan()
|
|
if dt >= config.conf.maa_gap * 3600:
|
|
base_scheduler.仓库扫描()
|
|
else:
|
|
logger.info(
|
|
f"仓库扫描未到时间,将在 {config.conf.maa_gap - dt // 3600}小时之内开始扫描"
|
|
)
|
|
if config.conf.maa_enable == 1:
|
|
subject = f"下次任务在{base_scheduler.tasks[0].time.strftime('%H:%M:%S')}"
|
|
context = f"下一次任务:{base_scheduler.tasks[0].plan}"
|
|
logger.info(context)
|
|
logger.info(subject)
|
|
body = task_template.render(
|
|
tasks=[
|
|
obj.format(timezone_offset)
|
|
for obj in base_scheduler.tasks
|
|
],
|
|
base_scheduler=base_scheduler,
|
|
)
|
|
send_message(body, subject)
|
|
base_scheduler.maa_plan_solver()
|
|
else:
|
|
remaining_time = (
|
|
base_scheduler.tasks[0].time - datetime.now()
|
|
).total_seconds()
|
|
subject = f"休息 {format_time(remaining_time)},到{base_scheduler.tasks[0].time.strftime('%H:%M:%S')}开始工作"
|
|
context = f"下一次任务:{base_scheduler.tasks[0].plan if len(base_scheduler.tasks[0].plan) != 0 else '空任务' if base_scheduler.tasks[0].type == '' else base_scheduler.tasks[0].type}"
|
|
logger.info(context)
|
|
logger.info(subject)
|
|
base_scheduler.task_count += 1
|
|
logger.info(f"第{base_scheduler.task_count}次任务结束")
|
|
if remaining_time > 0:
|
|
if remaining_time > 300:
|
|
if config.conf.close_simulator_when_idle:
|
|
restart_simulator(start=False)
|
|
elif config.conf.exit_game_when_idle:
|
|
config.device.exit()
|
|
body = task_template.render(
|
|
tasks=[
|
|
obj.format(timezone_offset)
|
|
for obj in base_scheduler.tasks
|
|
],
|
|
base_scheduler=base_scheduler,
|
|
)
|
|
send_message(body, subject)
|
|
config.idle = True
|
|
base_scheduler.sleep(remaining_time)
|
|
config.idle = False
|
|
base_scheduler.check_current_focus()
|
|
|
|
elif remaining_time > 0:
|
|
now_time = datetime.now().time()
|
|
try:
|
|
min_time = datetime.strptime(
|
|
config.conf.maa_rg_sleep_min, "%H:%M"
|
|
).time()
|
|
max_time = datetime.strptime(
|
|
config.conf.maa_rg_sleep_max, "%H:%M"
|
|
).time()
|
|
if max_time < min_time:
|
|
rg_sleep = now_time > min_time or now_time < max_time
|
|
else:
|
|
rg_sleep = min_time < now_time < max_time
|
|
except ValueError:
|
|
rg_sleep = False
|
|
|
|
if not rg_sleep:
|
|
if config.conf.RA:
|
|
config.recog.update()
|
|
base_scheduler.back_to_index()
|
|
ra_solver = ReclamationAlgorithm()
|
|
ra_solver.scheduler_stop_time = base_scheduler.tasks[0].time
|
|
ra_solver.run()
|
|
remaining_time = (
|
|
base_scheduler.tasks[0].time - datetime.now()
|
|
).total_seconds()
|
|
elif config.conf.SF:
|
|
config.recog.update()
|
|
base_scheduler.back_to_index()
|
|
sf_solver = SecretFront()
|
|
sf_solver.scheduler_stop_time = base_scheduler.tasks[0].time
|
|
sf_solver.run()
|
|
remaining_time = (
|
|
base_scheduler.tasks[0].time - datetime.now()
|
|
).total_seconds()
|
|
|
|
subject = f"休息 {format_time(remaining_time)},到{base_scheduler.tasks[0].time.strftime('%H:%M:%S')}开始工作"
|
|
context = f"下一次任务:{base_scheduler.tasks[0].plan}"
|
|
logger.info(context)
|
|
logger.info(subject)
|
|
base_scheduler.task_count += 1
|
|
logger.info(f"第{base_scheduler.task_count}次任务结束")
|
|
if remaining_time > 300:
|
|
if config.conf.close_simulator_when_idle:
|
|
restart_simulator(start=False)
|
|
elif config.conf.exit_game_when_idle:
|
|
config.device.exit()
|
|
body = task_template.render(
|
|
tasks=[
|
|
obj.format(timezone_offset) for obj in base_scheduler.tasks
|
|
],
|
|
base_scheduler=base_scheduler,
|
|
)
|
|
send_message(body, subject)
|
|
config.idle = True
|
|
base_scheduler.sleep(remaining_time)
|
|
config.idle = False
|
|
base_scheduler.check_current_focus()
|
|
|
|
base_scheduler.run()
|
|
reconnect_tries = 0
|
|
except MowerExit:
|
|
return
|
|
except (ConnectionError, ConnectionAbortedError, AttributeError) as e:
|
|
logger.exception(e)
|
|
reconnect_tries += 1
|
|
if reconnect_tries < reconnect_max_tries:
|
|
logger.warning("出现错误.尝试重启Mower")
|
|
connected = False
|
|
while not connected:
|
|
try:
|
|
base_scheduler = initialize([], base_scheduler)
|
|
break
|
|
except MowerExit:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
restart_simulator()
|
|
config.device.client.check_server_alive()
|
|
Session().connect()
|
|
if config.conf.droidcast.enable:
|
|
config.device.start_droidcast()
|
|
config.device.control.scrcpy = Scrcpy(config.device.client)
|
|
continue
|
|
continue
|
|
else:
|
|
raise e
|
|
except RuntimeError as e:
|
|
logger.exception(f"程序出错-尝试重启模拟器->{e}")
|
|
restart_simulator()
|
|
config.device.client.check_server_alive()
|
|
Session().connect()
|
|
if config.conf.droidcast.enable:
|
|
config.device.start_droidcast()
|
|
config.device.control.scrcpy = Scrcpy(config.device.client)
|
|
except Exception as e:
|
|
logger.exception(f"程序出错--->{e}")
|
|
config.recog.update()
|
|
|
|
|
|
def save_state(op_data, file="state.json"):
|
|
tmp_dir = get_path("@app/tmp")
|
|
if not tmp_dir.exists():
|
|
tmp_dir.mkdir()
|
|
state_file = tmp_dir / file
|
|
with open(state_file, "w") as f:
|
|
if op_data is not None:
|
|
json.dump(vars(op_data), f, default=str)
|
|
|
|
|
|
def load_state(file="state.json"):
|
|
state_file = get_path("@app/tmp") / file
|
|
if not state_file.exists():
|
|
return None
|
|
with open(state_file, "r") as f:
|
|
state = json.load(f)
|
|
operators = {k: Expr(v).eval() for k, v in state["operators"].items()}
|
|
for k, v in operators.items():
|
|
if not v.time_stamp == "None":
|
|
v.time_stamp = datetime.strptime(v.time_stamp, "%Y-%m-%d %H:%M:%S.%f")
|
|
else:
|
|
v.time_stamp = None
|
|
logger.info("基建配置已加载!")
|
|
return operators
|