Files
live-hub-py/services/yyb-worker/runtime/scripts/yyb-worker.py
T

811 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Small local HTTP worker for the YYB admin integration.
The worker owns per-job sessions and invokes the already verified protocol
scripts. It intentionally exposes QR images and state only; cookies and raw
payment links never leave the worker API.
"""
from __future__ import annotations
import argparse
import base64
import importlib.util
import json
import os
import re
import shutil
import subprocess
import sys
import threading
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
DEFAULT_DATA = ROOT / "config" / "worker-jobs"
DEFAULT_EVIDENCE_TTL_HOURS = 72
WORKER_KEY = os.environ.get("YYB_WORKER_KEY", "")
_jobs: dict[str, dict] = {}
_lock = threading.Lock()
from pyvm.payment_errors import describe_payment_failure # noqa: E402
def _payment_failure_message(job: dict, phase: str, fallback: str) -> str:
"""从阶段日志提取业务码,向管理端返回准确且可操作的失败信息。"""
detail = "\n".join(job.get("logs", []))
message = describe_payment_failure(str(job.get("provider", "")), phase, detail)
return message if message else fallback
def _load_selector():
path = ROOT / "scripts" / "select-peace-elite.py"
spec = importlib.util.spec_from_file_location("yyb_worker_selector", path)
if not spec or not spec.loader:
raise RuntimeError("无法加载和平精英选择器")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _job_dir(job_id: str) -> Path:
return Path(_jobs[job_id]["directory"])
def _cleanup_expired_jobs(data_dir: Path, ttl_hours: int) -> int:
"""清理超过保留期的任务证据;只处理 data_dir 的一级任务目录。"""
if ttl_hours <= 0 or not data_dir.exists():
return 0
deadline = time.time() - ttl_hours * 3600
removed = 0
for directory in data_dir.iterdir():
if not directory.is_dir() or directory.is_symlink():
continue
try:
if directory.stat().st_mtime >= deadline:
continue
shutil.rmtree(directory)
removed += 1
except OSError as exc:
print(f"无法清理过期任务证据 {directory.name}: {exc}", flush=True)
return removed
def _safe_log(job: dict, line: str) -> None:
# Do not persist cookies, payment URI, or long opaque tokens in the worker API.
clean = re.sub(r"weixin://\S+", "[付款链接已隐藏]", line)
clean = re.sub(r"HOLD_[A-Za-z0-9_-]+", "[订单已隐藏]", clean)
clean = re.sub(r"(订单\s*[:]\s*)\S+", r"\1[订单已隐藏]", clean)
clean = re.sub(
r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.I
)
clean = re.sub(
r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+",
"[敏感字段已隐藏]",
clean,
flags=re.I,
)
timestamp = time.strftime("%H:%M:%S")
with _lock:
job["logs"] = (job.get("logs", []) + [f"[{timestamp}] {clean.strip()}"])[-100:]
def _run_process(
job_id: str, command: list[str], phase: str, mark_success: bool = True
) -> int:
"""Run one stage process and return its exit code.
When ``mark_success`` is False the caller owns the post-success state
transition (used by the staged payment flow).
"""
job = _jobs[job_id]
environment = os.environ.copy()
environment.pop("NODE_OPTIONS", None)
# 子进程输出经管道实时转存日志;关闭块缓冲避免日志滞后。
environment["PYTHONUNBUFFERED"] = "1"
with _lock:
job["phase"] = phase
job["status"] = "running"
stage_started = time.monotonic()
_safe_log(job, f"[{phase}] 开始执行")
try:
process = subprocess.Popen(
command,
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
env=environment,
)
with _lock:
job["process_pid"] = process.pid
assert process.stdout is not None
for line in process.stdout:
_safe_log(job, line)
code = process.wait()
_safe_log(
job, f"[{phase}] 执行结束,耗时 {time.monotonic() - stage_started:.1f} 秒"
)
with _lock:
job["process_pid"] = None
if code != 0:
if job.get("status") == "stopped":
# 停止请求已 kill 本进程:保留停止语义,不被阶段失败覆盖。
job["message"] = "任务已停止"
else:
job["status"] = "failed"
job["phase"] = phase
job["message"] = f"{phase}失败(退出码 {code}"
elif phase == "login":
job["status"] = "ready"
job["phase"] = "selection"
job["message"] = "登录成功,请选择平台、点券、区服和角色"
elif mark_success:
job["status"] = "success"
job["phase"] = "completed"
job["message"] = "付款流程已完成"
return code
except Exception as exc: # noqa: BLE001
_safe_log(
job, f"[{phase}] 执行异常,耗时 {time.monotonic() - stage_started:.1f} 秒"
)
with _lock:
job["status"] = "failed"
job["message"] = str(exc)
return 1
def _start_login(job_id: str, provider: str, timeout: int) -> None:
job = _jobs[job_id]
directory = _job_dir(job_id)
session = directory / "mall-session.json"
qr = directory / ("qq-login.jpg" if provider == "qq" else "wechat-login.jpg")
command = [
sys.executable,
f"scripts/login-{provider}.py",
"--session",
str(session),
"--qr",
str(qr),
"--timeout",
str(timeout),
]
with _lock:
job["provider"] = provider
job["qr_path"] = str(qr)
job["session_path"] = str(session)
job["status"] = "waiting_login"
job["phase"] = "login"
threading.Thread(
target=_run_process, args=(job_id, command, "login"), daemon=True
).start()
def _selection_options(
job_id: str, platform: str, points: int | None, zone_id: str | None = None
) -> dict:
if job_id not in _jobs:
raise ValueError("任务不存在")
selector = _load_selector()
session = json.loads(
(_job_dir(job_id) / "mall-session.json").read_text(encoding="utf-8")
)
cookies = session.get("cookies", {})
products = selector.product_options(cookies, platform)
if points is not None and not any(
int(item["points"]) == points for item in products
):
raise ValueError("当前登录态不支持该点券档位")
product = (
next((item for item in products if int(item["points"]) == points), None)
if points
else None
)
if product is None:
product = products[0]
cmall = selector.Cmall(cookies, str(product["offer_id"]), platform)
zones = cmall.zones()
selected_zone = next(
(zone for zone in zones if str(zone["zone_id"]) == str(zone_id)), None
)
if zone_id and selected_zone is None:
raise ValueError("区服不存在")
selected_zone = selected_zone or (zones[0] if zones else None)
roles = cmall.roles(selected_zone["zone_id"]) if selected_zone else []
return {
"products": products,
"zones": zones,
"roles": roles,
"default_product": product,
"default_zone": selected_zone,
}
def _payment_stage(
job_id: str,
command: list[str],
phase: str,
running_message: str,
failed_message: str,
) -> bool:
"""Run one payment stage; return True on success without touching final state."""
job = _jobs[job_id]
with _lock:
if job.get("status") == "stopped":
return False
job["status"] = "running"
job["phase"] = phase
job["message"] = running_message
code = _run_process(job_id, command, phase, mark_success=False)
if code != 0:
with _lock:
if job.get("status") == "stopped":
return False
job["status"] = "failed"
job["phase"] = phase
job["message"] = _payment_failure_message(job, phase, failed_message)
return False
return True
PAYMENT_CHECK_INTERVAL = 3
PAYMENT_CHECK_TIMEOUT = 300
def _check_payment_once(job_id: str) -> int:
"""Read-only completion check; returns 0=confirmed, 1=not yet, 2=check failed."""
job = _jobs[job_id]
directory = _job_dir(job_id)
command = [
sys.executable,
"scripts/jsdom-pay.py",
"--check-only",
"--session",
str(directory / "mall-session.json"),
"--out-dir",
str(directory / "jsdom-order"),
]
environment = os.environ.copy()
environment.pop("NODE_OPTIONS", None)
check_started = time.monotonic()
_safe_log(job, "[到账检测] 开始执行")
try:
result = subprocess.run(
command,
cwd=ROOT,
capture_output=True,
text=True,
env=environment,
timeout=90,
)
except subprocess.TimeoutExpired:
_safe_log(
job, f"[到账检测] 执行超时,耗时 {time.monotonic() - check_started:.1f} 秒"
)
with _lock:
job["payment_last_checked_at"] = int(time.time())
return 2
for line in (result.stdout or "").splitlines():
_safe_log(job, line)
for line in (result.stderr or "").splitlines():
_safe_log(job, line)
_safe_log(
job, f"[到账检测] 执行结束,耗时 {time.monotonic() - check_started:.1f} 秒"
)
with _lock:
job["payment_last_checked_at"] = int(time.time())
return result.returncode
def _monitor_payment(job_id: str) -> None:
"""Background loop that re-checks completion until timeout or confirmation."""
job = _jobs[job_id]
deadline = time.monotonic() + PAYMENT_CHECK_TIMEOUT
while time.monotonic() < deadline:
time.sleep(PAYMENT_CHECK_INTERVAL)
with _lock:
if job.get("status") in {"stopped", "success"}:
return
code = _check_payment_once(job_id)
if code == 0:
with _lock:
job["status"] = "success"
job["phase"] = "completed"
job["message"] = "已确认到账,充值完成"
return
if code == 2:
with _lock:
job["message"] = "到账检测暂时失败,将继续重试"
with _lock:
job["status"] = "payment_timeout"
job["phase"] = "payment"
job["message"] = "付款码已生成,但未在时限内确认到账,请人工核对"
def _payment_flow(job_id: str, selection: dict) -> None:
"""Staged payment: create mall order -> create payment QR -> monitor arrival.
The QR is generated before any waiting, so the UI switches to
waiting_payment immediately; only a timeout moves to payment_timeout.
"""
job = _jobs[job_id]
directory = _job_dir(job_id)
session = directory / "mall-session.json"
response = directory / "mall-order-response.json"
output = directory / "jsdom-order"
with _lock:
job["status"] = "ordering"
job["phase"] = "payment"
job["message"] = "正在创建商城订单"
order_cmd = [
sys.executable,
"main.py",
"mall",
"auto",
"--session",
str(session),
"--order-template",
str(ROOT / "config" / "mall-order-template.json"),
"--quantity",
"1",
"--product-id",
str(selection["product_id"]),
"--offer-id",
str(selection["offer_id"]),
"--role-id",
str(selection["role_id"]),
"--role-name",
str(selection["role_name"]),
"--zone-id",
str(selection["zone_id"]),
"--zone-name",
str(selection["zone_name"]),
"--output",
str(response),
]
if selection.get("area"):
order_cmd.extend(["--area", str(selection["area"])])
if selection.get("partition"):
order_cmd.extend(["--partition", str(selection["partition"])])
if selection.get("order_pf"):
order_cmd.extend(["--pf", str(selection["order_pf"])])
if not _payment_stage(
job_id, order_cmd, "order", "正在创建商城订单", "创建商城订单失败"
):
return
pay_cmd = [
sys.executable,
"scripts/jsdom-pay.py",
"--session",
str(session),
"--mall-response",
str(response),
"--out-dir",
str(output),
"--zone-id",
str(selection["zone_id"]),
"--pf",
str(selection.get("order_pf", "")),
"--amount-fen",
str(selection["price_fen"]),
"--skip-payment-check",
]
if not _payment_stage(
job_id, pay_cmd, "payment", "正在生成微信付款码", "生成付款码失败"
):
return
meta_path = output / "payment-meta.json"
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
meta = {}
with _lock:
job["status"] = "waiting_payment"
job["phase"] = "payment"
job["message"] = "付款码已生成,请扫码付款"
job["payment_qr_created_at"] = meta.get("qr_created_at", int(time.time()))
_monitor_payment(job_id)
def _start_payment(job_id: str, selection: dict) -> None:
with _lock:
if _jobs[job_id].get("status") == "stopped":
return
_jobs[job_id]["status"] = "ordering"
_jobs[job_id]["phase"] = "payment"
_jobs[job_id]["message"] = "正在创建商城订单"
threading.Thread(
target=_payment_flow, args=(job_id, selection), daemon=True
).start()
def _stop_job(job_id: str) -> None:
if job_id not in _jobs:
raise ValueError("任务不存在")
job = _jobs[job_id]
pid = job.get("process_pid")
if pid:
try:
os.kill(int(pid), 15)
except ProcessLookupError:
pass
with _lock:
if job.get("status") == "waiting_payment":
job["status"] = "stopped"
job["phase"] = "payment"
job["message"] = "已放弃到账追踪;商城订单仍可能完成,请人工核对"
else:
job["status"] = "stopped"
job["phase"] = "stopped"
job["message"] = "任务已停止"
def _public_job(job_id: str) -> dict:
job = _jobs[job_id]
result = {
key: value
for key, value in job.items()
if key not in {"directory", "session_path", "process_pid"}
}
qr_path = job.get("qr_path", "")
if qr_path and Path(qr_path).exists():
qr_bytes = Path(qr_path).read_bytes()
result["qr_data"] = base64.b64encode(qr_bytes).decode("ascii")
result["qr_mime_type"] = (
"image/jpeg" if qr_bytes.startswith(b"\xff\xd8\xff") else "image/png"
)
output = Path(job["directory"]) / "jsdom-order"
for name in ("wechat-pay.png", "payment-status.json"):
path = None
if output.exists():
direct = output / name
path = direct if direct.exists() else next(output.glob(f"*/{name}"), None)
if path and name.endswith(".png"):
payment_qr_bytes = path.read_bytes()
result["payment_qr_data"] = base64.b64encode(payment_qr_bytes).decode(
"ascii"
)
result["payment_qr_mime_type"] = (
"image/jpeg"
if payment_qr_bytes.startswith(b"\xff\xd8\xff")
else "image/png"
)
elif path:
try:
status = json.loads(path.read_text(encoding="utf-8"))
matched = (
status.get("matched_completion")
if isinstance(status, dict)
else None
)
result["payment_status"] = {
"checked_at": status.get("checked_at")
if isinstance(status, dict)
else None,
"matched_completion": {
"is_finished": matched.get("is_finished"),
"status": matched.get("status"),
}
if isinstance(matched, dict)
else None,
}
except (OSError, json.JSONDecodeError):
pass
for key in ("payment_qr_created_at", "payment_last_checked_at"):
if job.get(key) is not None:
result[key] = job[key]
return result
class Handler(BaseHTTPRequestHandler):
server_version = "YYBWorker/1"
def _json(self, status: int, value: dict) -> None:
body = json.dumps(value, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _body(self) -> dict:
length = int(self.headers.get("Content-Length", "0"))
return json.loads(self.rfile.read(length) or b"{}")
def _authorized(self) -> bool:
if not WORKER_KEY:
return True
value = self.headers.get("Authorization", "")
return value == f"Bearer {WORKER_KEY}"
def do_POST(self) -> None: # noqa: N802
if not self._authorized():
return self._json(401, {"detail": "未授权"})
path = urlparse(self.path).path.strip("/").split("/")
try:
if path == ["v1", "jobs"]:
job_id = uuid.uuid4().hex[:16]
directory = DEFAULT_DATA / job_id
directory.mkdir(parents=True, exist_ok=True)
os.chmod(directory, 0o700)
_jobs[job_id] = {
"job_id": job_id,
"status": "created",
"phase": "login",
"logs": [],
"directory": str(directory),
"created_at": int(time.time()),
}
return self._json(201, _public_job(job_id))
if len(path) == 4 and path[:2] == ["v1", "jobs"] and path[3] == "login":
job_id = path[2]
body = self._body()
if job_id not in _jobs or body.get("provider") not in {"qq", "wechat"}:
return self._json(400, {"detail": "无效任务或登录方式"})
_start_login(job_id, body["provider"], int(body.get("timeout", 600)))
return self._json(202, _public_job(job_id))
if (
len(path) == 4
and path[:2] == ["v1", "jobs"]
and path[3] == "selection-options"
):
job_id = path[2]
body = self._body()
options = _selection_options(
job_id,
str(body.get("platform", "android")),
body.get("points"),
body.get("zone_id"),
)
_jobs[job_id]["selection_options"] = options
return self._json(200, options)
if len(path) == 4 and path[:2] == ["v1", "jobs"] and path[3] == "selection":
job_id = path[2]
body = self._body()
required = (
"platform",
"points",
"product_id",
"role_id",
"role_name",
"zone_id",
)
if job_id not in _jobs or any(not body.get(key) for key in required):
return self._json(400, {"detail": "选择参数不完整"})
selector = _load_selector()
if body["platform"] not in selector.PLATFORMS:
return self._json(400, {"detail": "不支持的平台"})
session_path = _job_dir(job_id) / "mall-session.json"
cookies = json.loads(session_path.read_text(encoding="utf-8")).get(
"cookies", {}
)
product = next(
(
item
for item in selector.product_options(cookies, body["platform"])
if str(item["product_id"]) == str(body["product_id"])
and int(item["points"]) == int(body["points"])
),
None,
)
if product is None:
return self._json(400, {"detail": "商品已失效,请重新选择"})
cmall = selector.Cmall(
cookies, str(product["offer_id"]), body["platform"]
)
zone = next(
(
item
for item in cmall.zones()
if str(item["zone_id"]) == str(body["zone_id"])
),
None,
)
if zone is None:
return self._json(400, {"detail": "区服已失效,请重新选择"})
role = next(
(
item
for item in cmall.roles(zone["zone_id"])
if str(item["role_id"]) == str(body["role_id"])
),
None,
)
if role is None or role.get("ban_status") == "1":
return self._json(400, {"detail": "角色不可充值,请重新选择"})
_jobs[job_id]["selection"] = {
"platform": body["platform"],
"points": product["points"],
"price_fen": product["price_fen"],
"product_id": product["product_id"],
"offer_id": product["offer_id"],
"zone_id": zone["zone_id"],
"zone_name": zone["name"],
"role_id": role["role_id"],
"role_name": role["name"],
"area": role.get("area", ""),
"partition": role.get("partition", ""),
"order_pf": selector.PLATFORMS[body["platform"]]["order_pf"],
}
_jobs[job_id]["phase"] = "payment"
result = _public_job(job_id)
result["selection"] = _jobs[job_id]["selection"]
return self._json(200, result)
if len(path) == 4 and path[:2] == ["v1", "jobs"] and path[3] == "payment":
job_id = path[2]
if job_id not in _jobs:
return self._json(400, {"detail": "任务不存在"})
job = _jobs[job_id]
with _lock:
if job.get("status") == "stopped":
return self._json(400, {"detail": "任务已停止,不能生成付款码"})
if job.get("status") in {
"ordering",
"waiting_payment",
"payment_timeout",
} or job.get("process_pid"):
return self._json(
400, {"detail": "已有进行中的支付流程,请勿重复操作"}
)
if not job.get("selection"):
return self._json(400, {"detail": "请先完成角色选择"})
_start_payment(job_id, job["selection"])
return self._json(202, _public_job(job_id))
if (
len(path) == 5
and path[:2] == ["v1", "jobs"]
and path[3:5] == ["payment", "check"]
):
job_id = path[2]
if job_id not in _jobs:
return self._json(400, {"detail": "任务不存在"})
job = _jobs[job_id]
with _lock:
if job.get("status") not in {"waiting_payment", "payment_timeout"}:
return self._json(400, {"detail": "当前任务状态不支持检测到账"})
code = _check_payment_once(job_id)
with _lock:
if code == 0:
job["status"] = "success"
job["phase"] = "completed"
job["message"] = "已确认到账,充值完成"
elif code == 1:
# 保持 payment_timeout:检测由人工反复触发,避免无人监控的 waiting_payment。
job["message"] = "仍未确认到账,请继续人工核对"
else:
job["message"] = "到账检测暂时失败,请稍后重试"
return self._json(200, _public_job(job_id))
if len(path) == 4 and path[:2] == ["v1", "jobs"] and path[3] == "stop":
_stop_job(path[2])
return self._json(200, _public_job(path[2]))
return self._json(404, {"detail": "接口不存在"})
except Exception as exc: # noqa: BLE001
return self._json(500, {"detail": str(exc)})
def do_GET(self) -> None: # noqa: N802
if urlparse(self.path).path == "/health":
return self._json(200, {"status": "ok"})
if not self._authorized():
return self._json(401, {"detail": "未授权"})
path = urlparse(self.path).path.strip("/").split("/")
if len(path) == 3 and path[:2] == ["v1", "jobs"] and path[2] in _jobs:
return self._json(200, _public_job(path[2]))
return self._json(404, {"detail": "接口不存在"})
def log_message(self, fmt: str, *args) -> None:
return
def _restore_jobs(data_dir: Path) -> int:
"""Rebuild in-memory jobs from task directories after a restart.
Only payment-phase jobs with a persisted QR baseline are restored so that
the payment QR stays visible and the read-only check can be re-run.
"""
restored = 0
for directory in data_dir.iterdir():
if not directory.is_dir():
continue
job_id = directory.name
if job_id in _jobs:
continue
session = directory / "mall-session.json"
output = directory / "jsdom-order"
meta_path = output / "payment-meta.json"
if not session.exists():
continue
if not meta_path.exists():
_jobs[job_id] = {
"job_id": job_id,
"status": "ready",
"phase": "selection",
"logs": ["服务重启,已从任务目录恢复登录会话"],
"directory": str(directory),
"created_at": int(time.time()),
"message": "已恢复:登录会话有效,请选择充值信息",
}
restored += 1
continue
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
status = "waiting_payment"
phase = "payment"
message = "已恢复:付款码已生成,可重新检测到账"
last_checked = None
status_path = output / "payment-status.json"
if status_path.exists():
try:
record = json.loads(status_path.read_text(encoding="utf-8"))
last_checked = record.get("checked_at")
if record.get("matched_completion"):
status = "success"
phase = "completed"
message = "已恢复:已确认到账"
except (OSError, json.JSONDecodeError):
pass
_jobs[job_id] = {
"job_id": job_id,
"status": status,
"phase": phase,
"logs": ["服务重启,已从任务目录恢复本任务"],
"directory": str(directory),
"created_at": int(time.time()),
"message": message,
"payment_qr_created_at": meta.get("qr_created_at"),
"payment_last_checked_at": last_checked,
}
restored += 1
return restored
def main() -> int:
global DEFAULT_DATA, WORKER_KEY
parser = argparse.ArgumentParser(description="YYB admin worker")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8810)
parser.add_argument("--data-dir", type=Path, default=DEFAULT_DATA)
parser.add_argument(
"--evidence-ttl-hours",
type=int,
default=DEFAULT_EVIDENCE_TTL_HOURS,
help="任务原始证据保留时长;0 表示不自动清理",
)
parser.add_argument(
"--key", default=None, help="HTTP Bearer 鉴权密钥;默认读取 YYB_WORKER_KEY"
)
args = parser.parse_args()
# 强制绝对路径:子进程以 ROOT 为 cwd,相对路径会让任务文件写到错误位置。
DEFAULT_DATA = Path(args.data_dir).resolve()
if args.key is not None:
WORKER_KEY = args.key
if args.host not in {"127.0.0.1", "localhost", "::1"} and not WORKER_KEY:
parser.error("监听非本机地址时必须设置 --key 或 YYB_WORKER_KEY")
if args.evidence_ttl_hours < 0:
parser.error("--evidence-ttl-hours 不能小于 0")
DEFAULT_DATA.mkdir(parents=True, exist_ok=True)
os.chmod(DEFAULT_DATA, 0o700)
removed = _cleanup_expired_jobs(DEFAULT_DATA, args.evidence_ttl_hours)
if removed:
print(
f"已清理 {removed} 个过期任务证据(保留期 {args.evidence_ttl_hours} 小时)",
flush=True,
)
restored = _restore_jobs(DEFAULT_DATA)
if restored:
print(f"已从任务目录恢复 {restored} 个支付任务", flush=True)
server = ThreadingHTTPServer((args.host, args.port), Handler)
print(f"YYB worker listening on {args.host}:{args.port}", flush=True)
server.serve_forever()
return 0
if __name__ == "__main__":
raise SystemExit(main())