Files
abot/main.py

163 lines
5.5 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
# -*- coding: utf-8 -*-
import asyncio
import threading
from utils.decorator.async_job import async_job
from utils.markdown_to_image import warmup_md2img_browser
from configuration import Config
from robot import Robot
from loguru import logger
from utils.sehuatang.sehuatang_bot import SehuatangCrawler
# 普通日志不附带 traceback避免 debug/info 文件被异常堆栈刷屏。
def _plain_log_format(record):
record["exception"] = None
return "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {name}:{function}:{line} - {message}\n"
def _error_log_format(record):
return (
"{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {name}:{function}:{line} - {message}\n"
"{exception}"
)
# INFO 日志(包含 INFO、DEBUG但不包含 WARNING、ERROR
logger.add(
f"logs/wx_info.log",
level="INFO",
filter=lambda record: record["level"].name in ["INFO", "DEBUG"],
rotation="10 MB",
retention="7 days",
encoding="utf-8",
format=_plain_log_format,
backtrace=False,
diagnose=False,
)
# ERROR 日志(仅 ERROR 及以上)
logger.add(
f"logs/wx_error.log",
level="ERROR",
rotation="10 MB",
retention="7 days",
encoding="utf-8",
format=_error_log_format,
backtrace=True,
diagnose=True,
)
# ERROR 日志(仅 ERROR 及以上)
logger.add(
f"logs/wx_debug.log",
level="DEBUG",
rotation="10 MB",
retention="7 days",
encoding="utf-8",
format=_plain_log_format,
backtrace=False,
diagnose=False,
)
def _log_config_validation(config: Config) -> None:
"""输出启动期配置校验结果。"""
validation_report = config.get_validation_report()
errors = list(validation_report.get("errors", []) or [])
warnings = list(validation_report.get("warnings", []) or [])
logger.info(
"配置加载完成: "
f"environment={config.environment}, "
f"plugin_dir={config.plugin_dir}, "
f"errors={len(errors)}, "
f"warnings={len(warnings)}"
)
# 这里只打印脱敏后的配置快照:
# 1. 便于定位“到底加载了哪套配置”;
# 2. 同时避免把数据库密码、API Key 再写进日志;
# 3. 放在 DEBUG 级别,默认不会刷屏主日志。
logger.debug(f"配置脱敏快照: {config.get_sanitized_snapshot()}")
for warning in warnings:
logger.warning(
f"配置告警[{warning.get('code', 'unknown')}] "
f"{warning.get('path', 'root')}: {warning.get('message', '')}"
)
for error in errors:
logger.error(
f"配置错误[{error.get('code', 'unknown')}] "
f"{error.get('path', 'root')}: {error.get('message', '')}"
)
if errors:
raise ValueError("启动终止:存在未修复的致命配置错误,请先修正 config.yaml 或环境变量。")
def main():
config = Config()
_log_config_validation(config)
# 创建机器人实例
robot = Robot(config)
robot.LOG.info(f"ABOT服务 正在启动...")
# 初始化并启动wechat_ipad客户端
if robot.init_wechat_ipad():
# 这里刻意不用“登录成功”措辞:
# 1. 主线程当前只能确认 provider 已成功创建、子线程已进入运行流程;
# 2. 真正的登录成功仍取决于缓存唤醒或扫码流程,日志会在 wechat 线程里继续输出;
# 3. 这样可以避免运维把“线程已启动”误认为“账号已完全在线”。
robot.LOG.info("wechat_ipad客户端线程已启动等待登录态完成")
else:
robot.LOG.error("wechat_ipad客户端启动失败")
# 注册定时任务
jobs(robot)
# 启动Dashboard服务器
try:
# 创建Dashboard服务器实例共享robot对象
from admin.dashboard.server import DashboardServer
dashboard_server = DashboardServer(robot_instance=robot)
# 在单独的线程中启动Dashboard服务器
dashboard_thread = threading.Thread(target=dashboard_server.run, daemon=True)
dashboard_thread.start()
robot.LOG.info(f"Dashboard服务器已在 http://{dashboard_server.host}:{dashboard_server.port} 启动")
except Exception as e:
robot.LOG.error(f"Dashboard服务器启动失败: {e}")
# 启动后在“调度器同一事件循环”中预热 Markdown 转图浏览器。
# 这样可确保预热得到的常驻浏览器与后续截图任务复用同一 loop避免跨 loop 句柄失效。
try:
async def _warmup_md2img():
ok = await warmup_md2img_browser(timeout_seconds=60)
if ok:
robot.LOG.info("Markdown 转图浏览器预热成功(调度器事件循环)")
else:
robot.LOG.warning("Markdown 转图浏览器预热失败,运行期将按需重试")
async_job.add_startup_job(_warmup_md2img, name="md2img_warmup")
except Exception as e:
robot.LOG.error(f"注册 Markdown 转图预热任务失败: {e}")
robot.LOG.info(f"=" * 50)
asyncio.run(async_job.run_all())
# 让机器人一直跑
robot.keep_running_and_block_process()
def jobs(robot: Robot):
# 系统级定时任务统一改为数据库驱动,不再在 main.py 里硬编码维护。
# 这里保留入口,只负责按表配置重新加载,便于运行时刷新。
if hasattr(robot, "system_job_loader") and robot.system_job_loader:
robot.system_job_loader.reload_from_db()
if __name__ == "__main__":
main()