551 lines
14 KiB
Python
Executable file
551 lines
14 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
import datetime
|
||
import json
|
||
import mimetypes
|
||
import os
|
||
import pathlib
|
||
import sys
|
||
import time
|
||
from functools import wraps
|
||
from io import BytesIO
|
||
from threading import Thread
|
||
|
||
import pytz
|
||
from flask import Flask, abort, request, send_file, send_from_directory
|
||
from flask_cors import CORS
|
||
from flask_sock import Sock
|
||
from tzlocal import get_localzone
|
||
from werkzeug.exceptions import NotFound
|
||
|
||
from mower import __system__
|
||
from mower.solvers.infra.report import ReportSolver
|
||
from mower.utils import config
|
||
from mower.utils.log import logger
|
||
from mower.utils.path import get_path
|
||
|
||
mimetypes.add_type("text/html", ".html")
|
||
mimetypes.add_type("text/css", ".css")
|
||
mimetypes.add_type("application/javascript", ".js")
|
||
|
||
app = Flask(__name__, static_folder="ui/dist", static_url_path="")
|
||
sock = Sock(app)
|
||
CORS(app)
|
||
|
||
mower_thread = None
|
||
log_lines = []
|
||
ws_connections = []
|
||
|
||
|
||
def read_log():
|
||
global log_lines
|
||
global ws_connections
|
||
|
||
while True:
|
||
msg = config.log_queue.get()
|
||
log_lines.append(msg)
|
||
log_lines = log_lines[-100:]
|
||
for ws in ws_connections:
|
||
ws.send(msg)
|
||
|
||
|
||
Thread(target=read_log, daemon=True).start()
|
||
|
||
|
||
def post_require_token(f):
|
||
@wraps(f)
|
||
def decorated_function(*args, **kwargs):
|
||
if (
|
||
request.method == "POST"
|
||
and hasattr(app, "token")
|
||
and request.headers.get("token", "") != app.token
|
||
):
|
||
abort(403)
|
||
return f(*args, **kwargs)
|
||
|
||
return decorated_function
|
||
|
||
|
||
def get_require_token(f):
|
||
@wraps(f)
|
||
def decorated_function(*args, **kwargs):
|
||
if (
|
||
request.method == "GET"
|
||
and hasattr(app, "token")
|
||
and request.headers.get("token", "") != app.token
|
||
):
|
||
abort(403)
|
||
return f(*args, **kwargs)
|
||
|
||
return decorated_function
|
||
|
||
|
||
@app.route("/<path:path>")
|
||
def serve_index(path):
|
||
return send_from_directory("ui/dist", path)
|
||
|
||
|
||
@app.errorhandler(404)
|
||
def not_found(e):
|
||
if (path := request.path).startswith("/docs"):
|
||
try:
|
||
return send_from_directory("ui/dist" + path, "index.html")
|
||
except NotFound:
|
||
return "<h1>404 Not Found</h1>", 404
|
||
return send_from_directory("ui/dist", "index.html")
|
||
|
||
|
||
@app.route("/conf", methods=["GET", "POST"])
|
||
@get_require_token
|
||
@post_require_token
|
||
def load_config():
|
||
if request.method == "GET":
|
||
return config.conf.model_dump()
|
||
else:
|
||
config.conf = config.Conf(**request.json)
|
||
config.save_conf()
|
||
return "New config saved!"
|
||
|
||
|
||
@app.route("/plan", methods=["GET", "POST"])
|
||
@post_require_token
|
||
def load_plan_from_json():
|
||
if request.method == "GET":
|
||
return config.plan.model_dump(exclude_none=True)
|
||
else:
|
||
config.plan = config.PlanModel(**request.json)
|
||
config.save_plan()
|
||
return "New plan saved。"
|
||
|
||
|
||
@app.route("/operator")
|
||
def operator_list():
|
||
from mower.data import agent_list
|
||
|
||
return agent_list
|
||
|
||
|
||
@app.route("/shop")
|
||
def shop_list():
|
||
from mower.data import shop_items
|
||
|
||
return list(shop_items.keys())
|
||
|
||
|
||
@app.route("/activity")
|
||
def activity():
|
||
from mower.solvers.navigation.activity import ActivityNavigation
|
||
|
||
location = ActivityNavigation.location
|
||
if isinstance(location, dict):
|
||
return list(location.keys())
|
||
elif isinstance(location, list):
|
||
return location
|
||
else:
|
||
return []
|
||
|
||
|
||
@app.route("/depot/readdepot")
|
||
def read_depot():
|
||
from mower.solvers.depot_reader import DepotManager
|
||
|
||
a = DepotManager()
|
||
return a.读取仓库()
|
||
|
||
|
||
@app.route("/running")
|
||
def running():
|
||
return "true" if mower_thread and mower_thread.is_alive() else "false"
|
||
|
||
|
||
@app.route("/start")
|
||
@get_require_token
|
||
def start():
|
||
global mower_thread
|
||
global log_lines
|
||
|
||
if mower_thread and mower_thread.is_alive():
|
||
return "false"
|
||
|
||
# 创建 tmp 文件夹
|
||
tmp_dir = get_path("@app/tmp")
|
||
tmp_dir.mkdir(exist_ok=True)
|
||
|
||
config.stop_mower.clear()
|
||
config.operators = {}
|
||
|
||
from mower.__main__ import main
|
||
|
||
mower_thread = Thread(target=main, daemon=True)
|
||
mower_thread.start()
|
||
|
||
config.idle = False
|
||
|
||
log_lines = []
|
||
|
||
return "true"
|
||
|
||
|
||
@app.route("/stop")
|
||
@get_require_token
|
||
def stop():
|
||
global mower_thread
|
||
|
||
if mower_thread is None:
|
||
return "true"
|
||
|
||
config.stop_mower.set()
|
||
|
||
mower_thread.join(10)
|
||
if mower_thread.is_alive():
|
||
logger.error("Mower线程仍在运行")
|
||
return "false"
|
||
else:
|
||
logger.info("成功停止mower线程")
|
||
mower_thread = None
|
||
config.idle = True
|
||
return "true"
|
||
|
||
|
||
@app.route("/stop-maa")
|
||
@get_require_token
|
||
def stop_maa():
|
||
global mower_thread
|
||
|
||
if mower_thread is None:
|
||
return "true"
|
||
|
||
config.stop_maa.set()
|
||
return "OK"
|
||
|
||
|
||
@sock.route("/log")
|
||
def log(ws):
|
||
global ws_connections
|
||
global log_lines
|
||
|
||
ws.send("\n".join(log_lines))
|
||
ws_connections.append(ws)
|
||
|
||
from simple_websocket import ConnectionClosed
|
||
|
||
try:
|
||
while True:
|
||
ws.receive()
|
||
except ConnectionClosed:
|
||
ws_connections.remove(ws)
|
||
|
||
|
||
def conn_send(text):
|
||
from mower.utils import config
|
||
|
||
if not config.webview_process.is_alive():
|
||
return ""
|
||
|
||
config.parent_conn.send(text)
|
||
return config.parent_conn.recv()
|
||
|
||
|
||
@app.route("/dialog/file")
|
||
@get_require_token
|
||
def open_file_dialog():
|
||
return conn_send("file")
|
||
|
||
|
||
@app.route("/dialog/folder")
|
||
@get_require_token
|
||
def open_folder_dialog():
|
||
return conn_send("folder")
|
||
|
||
|
||
@app.route("/import", methods=["POST"])
|
||
@post_require_token
|
||
def import_from_image():
|
||
img = request.files["img"]
|
||
if img.mimetype == "application/json":
|
||
data = json.load(img)
|
||
else:
|
||
try:
|
||
from PIL import Image
|
||
|
||
from mower.utils import qrcode
|
||
|
||
img = Image.open(img)
|
||
data = qrcode.decode(img)
|
||
except Exception as e:
|
||
msg = f"排班表导入失败:{e}"
|
||
logger.exception(msg)
|
||
return msg
|
||
if data:
|
||
config.plan = config.PlanModel(**data)
|
||
config.save_plan()
|
||
return "排班已加载"
|
||
else:
|
||
return "排班表导入失败!"
|
||
|
||
|
||
@app.route("/sss-copilot", methods=["GET", "POST"])
|
||
@post_require_token
|
||
def upload_sss_copilot():
|
||
copilot = get_path("@app/sss.json")
|
||
if request.method == "GET":
|
||
if copilot.is_file():
|
||
with copilot.open("r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
else:
|
||
return {"exists": False}
|
||
else:
|
||
print(request.files)
|
||
data = request.files["copilot"]
|
||
data.save(copilot)
|
||
data.seek(0)
|
||
data = json.load(data)
|
||
return {
|
||
"exists": True,
|
||
"title": data["doc"]["title"],
|
||
"details": data["doc"]["details"],
|
||
"operators": data["opers"],
|
||
}
|
||
|
||
|
||
@app.route("/dialog/save/img", methods=["POST"])
|
||
@post_require_token
|
||
def save_file_dialog():
|
||
img = request.files["img"]
|
||
|
||
from PIL import Image
|
||
|
||
from mower.utils import qrcode
|
||
|
||
upper = Image.open(img)
|
||
|
||
img = qrcode.export(
|
||
config.plan.model_dump(exclude_none=True), upper, config.conf.theme
|
||
)
|
||
buffer = BytesIO()
|
||
img.save(buffer, format="JPEG")
|
||
buffer.seek(0)
|
||
return send_file(buffer, "image/jpeg")
|
||
|
||
|
||
@app.route("/export-json")
|
||
def export_json():
|
||
return send_file(config.plan_path)
|
||
|
||
|
||
@app.route("/check-maa")
|
||
@get_require_token
|
||
def get_maa_adb_version():
|
||
try:
|
||
asst_path = os.path.dirname(
|
||
pathlib.Path(config.conf.maa_path) / "Python" / "asst"
|
||
)
|
||
if asst_path not in sys.path:
|
||
sys.path.append(asst_path)
|
||
from asst.asst import Asst
|
||
|
||
Asst.load(config.conf.maa_path)
|
||
asst = Asst()
|
||
version = asst.get_version()
|
||
asst.set_instance_option(2, config.conf.maa_touch_option)
|
||
if asst.connect(config.conf.maa_adb_path, config.conf.adb):
|
||
maa_msg = f"Maa {version} 加载成功"
|
||
else:
|
||
maa_msg = "连接失败,请检查Maa日志!"
|
||
except Exception as e:
|
||
maa_msg = "Maa加载失败:" + str(e)
|
||
logger.exception(maa_msg)
|
||
return maa_msg
|
||
|
||
|
||
@app.route("/maa-conn-preset")
|
||
@get_require_token
|
||
def get_maa_conn_presets():
|
||
try:
|
||
with open(
|
||
os.path.join(config.conf.maa_path, "resource", "config.json"),
|
||
"r",
|
||
encoding="utf-8",
|
||
) as f:
|
||
presets = [i["configName"] for i in json.load(f)["connection"]]
|
||
except Exception as e:
|
||
logger.exception(e)
|
||
presets = []
|
||
return presets
|
||
|
||
|
||
@app.route("/record/getMoodRatios")
|
||
def get_mood_ratios():
|
||
from mower.solvers.infra import record
|
||
|
||
return record.get_mood_ratios()
|
||
|
||
|
||
@app.route("/getwatermark")
|
||
def getwatermark():
|
||
from mower.__init__ import __version__
|
||
|
||
return __version__
|
||
|
||
|
||
def str2date(target: str):
|
||
try:
|
||
return datetime.datetime.strptime(target, "%Y-%m-%d").date()
|
||
except ValueError:
|
||
return datetime.datetime.strptime(target, "%Y/%m/%d").date()
|
||
|
||
|
||
def date2str(target: datetime.date):
|
||
try:
|
||
return datetime.datetime.strftime(target, "%Y-%m-%d")
|
||
except ValueError:
|
||
return datetime.datetime.strftime(target, "%Y/%m/%d")
|
||
|
||
|
||
@app.route("/report/getReportData")
|
||
def get_report_data():
|
||
a = ReportSolver()
|
||
return a.get_report_data()
|
||
|
||
|
||
@app.route("/report/getOrundumData")
|
||
def get_orundum_data():
|
||
a = ReportSolver()
|
||
return a.get_orundum_data()
|
||
|
||
|
||
@app.route("/test-email")
|
||
@get_require_token
|
||
def test_email():
|
||
from mower.utils.email import Email
|
||
|
||
email = Email("mower测试邮件", config.conf.mail_subject + "测试邮件", None)
|
||
try:
|
||
email.send()
|
||
except Exception as e:
|
||
msg = "邮件发送失败!\n" + str(e)
|
||
logger.exception(msg)
|
||
return msg
|
||
return "邮件发送成功!"
|
||
|
||
|
||
@app.route("/test-custom-screenshot")
|
||
@get_require_token
|
||
def test_custom_screenshot():
|
||
import base64
|
||
import subprocess
|
||
|
||
import cv2
|
||
import numpy as np
|
||
|
||
command = config.conf.custom_screenshot.command
|
||
|
||
start = time.time()
|
||
data = subprocess.check_output(
|
||
command,
|
||
shell=True,
|
||
creationflags=subprocess.CREATE_NO_WINDOW if __system__ == "windows" else 0,
|
||
)
|
||
end = time.time()
|
||
elapsed = int((end - start) * 1000)
|
||
|
||
data = np.frombuffer(data, np.uint8)
|
||
data = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
||
_, data = cv2.imencode(".jpg", data, [int(cv2.IMWRITE_JPEG_QUALITY), 75])
|
||
data = base64.b64encode(data)
|
||
data = data.decode("ascii")
|
||
|
||
return {"elapsed": elapsed, "screenshot": data}
|
||
|
||
|
||
@app.route("/check-skland")
|
||
@get_require_token
|
||
def test_skland():
|
||
from mower.solvers.skland import SKLand
|
||
|
||
return SKLand().test_connect()
|
||
|
||
|
||
@app.route("/idle")
|
||
def get_idle_status():
|
||
return str(config.idle)
|
||
|
||
|
||
@app.route("/task", methods=["GET", "POST"])
|
||
@post_require_token
|
||
def get_count():
|
||
from mower import __main__
|
||
|
||
base_scheduler = __main__.base_scheduler
|
||
|
||
if request.method == "POST":
|
||
from mower.data import agent_list
|
||
from mower.utils.operators import SkillUpgradeSupport
|
||
from mower.utils.scheduler_task import SchedulerTask, TaskTypes
|
||
|
||
try:
|
||
req = request.json
|
||
task = req["task"]
|
||
logger.debug(f"收到新增任务请求:{req}")
|
||
if base_scheduler and mower_thread.is_alive():
|
||
if task:
|
||
utc_time = datetime.datetime.strptime(
|
||
task["time"], "%Y-%m-%dT%H:%M:%S.%f%z"
|
||
)
|
||
task_time = (
|
||
utc_time.replace(tzinfo=pytz.utc)
|
||
.astimezone(get_localzone())
|
||
.replace(tzinfo=None)
|
||
)
|
||
new_task = SchedulerTask(
|
||
time=task_time,
|
||
task_plan=task["plan"],
|
||
task_type=task["task_type"],
|
||
meta_data=task["meta_data"],
|
||
)
|
||
if base_scheduler.find_next_task(
|
||
compare_time=task_time, compare_type="="
|
||
):
|
||
raise Exception("找到同时间任务请勿重复添加")
|
||
if new_task.type == TaskTypes.SKILL_UPGRADE:
|
||
supports = []
|
||
for s in req["upgrade_support"]:
|
||
if (
|
||
s["name"] not in agent_list
|
||
or s["swap_name"] not in agent_list
|
||
):
|
||
raise Exception("干员名不正确")
|
||
supports.append(
|
||
SkillUpgradeSupport(
|
||
name=s["name"],
|
||
skill_level=s["skill_level"],
|
||
efficiency=s["efficiency"],
|
||
match=s["match"],
|
||
swap_name=s["swap_name"],
|
||
)
|
||
)
|
||
if len(supports) == 0:
|
||
raise Exception("请添加专精工具人")
|
||
base_scheduler.op_data.skill_upgrade_supports = supports
|
||
logger.error("更新专精工具人完毕")
|
||
base_scheduler.tasks.append(new_task)
|
||
logger.debug(f"成功:{str(new_task)}")
|
||
return "添加任务成功!"
|
||
raise Exception("添加任务失败!!")
|
||
except Exception as e:
|
||
logger.exception(f"添加任务失败:{str(e)}")
|
||
return str(e)
|
||
else:
|
||
if base_scheduler and mower_thread and mower_thread.is_alive():
|
||
from jsonpickle import encode
|
||
|
||
return [
|
||
json.loads(
|
||
encode(
|
||
i,
|
||
unpicklable=False,
|
||
)
|
||
)
|
||
for i in base_scheduler.tasks
|
||
]
|
||
else:
|
||
return []
|