from datetime import datetime
from typing import Any
from zoneinfo import ZoneInfo
from aiocqhttp.exceptions import ActionFailed
from astrbot.api import logger
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.core import html_renderer
from astrbot.core.utils.session_waiter import (
SessionController,
session_waiter,
)
from .bilei_data import BiLeidata
from .cache import CacheService
from .event_push import EventPushService
from .jx3api_data import JX3APIService
from .jx3box_data import JX3BOXService
class MessageBuilder:
"""回复消息构建"""
IMAGE_RENDER_HANDLERS = frozenset(
{
"helps", "richangyuche", "qiongyewei", "pifenghui", "yunchongshe",
"chutianshe", "guanaishouling", "zhenyingevent", "yanhuachaxun",
"zhanji", "mingjianpaihang", "mingjiantongji", "kuafumingjian",
"wulinzhengba", "bukairongyu", "jianghulangke", "juedoutiaozhan",
"banghuipaihang", "zhenyingpaihang", "qitapaihang", "shilianpaixing",
"zhengyingpaimai", "dilujilu", "jinjia", "wujia", "chengbeng",
"bangzhanjilu", "shapan", "zhueevent", "qiyuhuizong", "weizuoqiyu",
"jinqiqiyu", "juesheqiyu", "qiyutongji", "qiyugonglue", "jingnai",
"baizhan", "chengjiu", "zilipaixing", "jineng", "qixue", "liaotian",
"xiaoyao", "huajia", "zhuangshi", "qiwu", "baishi", "shoutu",
"tuanduizhaomu", "tuanzhang", "tuanpai", "zhuangtai", "fubeng",
"diaoluo", "hong", "zili", "jiaoyihang", "bilei_all", "bilei_select",
}
)
_RENDER_FORMATS = {"jpeg", "png"}
_DATA_TIME_MARKER = "data-jx3-data-time"
_DATA_TIME_ZONE = ZoneInfo("Asia/Shanghai")
_SESSION_SCOPED_IMAGE_NAMES = frozenset({"避雷查看", "避雷查询"})
_DEVICE_SCALE_FACTOR_LEVELS = {
1.0: "normal",
1.3: "high",
1.8: "ultra",
}
def __init__(self,
jx3api: JX3APIService,
jx3box: JX3BOXService,
bilei: BiLeidata,
event_push: EventPushService,
icons: dict[str, dict[str, str]],
render_config: dict[str, Any] | None = None,
cache: CacheService | None = None,
):
self.jx3api = jx3api
self.jx3box = jx3box
self.bilei = bilei
self.event_push = event_push
self.icons = icons
self.render_config = render_config if isinstance(render_config, dict) else {}
self.cache = cache
def _build_render_options(
self,
overrides: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""根据插件配置构造 AstrBot HTML 截图参数。"""
image_format = str(self.render_config.get("format", "jpeg")).lower()
if image_format not in self._RENDER_FORMATS:
image_format = "jpeg"
try:
device_scale_factor = float(
self.render_config.get("device_scale_factor", 1.3)
)
except (TypeError, ValueError):
device_scale_factor = 1.3
scale_factor_level = self._DEVICE_SCALE_FACTOR_LEVELS.get(
device_scale_factor,
"high",
)
try:
jpeg_quality = int(self.render_config.get("jpeg_quality", 100))
except (TypeError, ValueError):
jpeg_quality = 100
jpeg_quality = max(1, min(100, jpeg_quality))
options: dict[str, Any] = {
"quality": jpeg_quality,
"device_scale_factor_level": scale_factor_level,
"scale": "device",
"full_page": True,
"omit_background": False,
"type": image_format,
}
options.update(overrides or {})
if options.get("type") == "png":
options.pop("quality", None)
return options
async def html_render(
self,
tmpl: str,
data: dict,
return_url=True,
options: dict | None = None,
) -> str:
"""渲染 HTML"""
return await html_renderer.render_custom_template(
tmpl,
data,
return_url=return_url,
options=self._build_render_options(options),
)
@staticmethod
def _format_data_time(result: dict[str, Any] | None = None) -> str:
"""优先使用接口缓存创建时间,否则使用本次数据生成时间。"""
cache_metadata = (result or {}).get("_cache") or {}
timestamp = cache_metadata.get("created_at")
try:
numeric_timestamp = float(timestamp)
if numeric_timestamp > 10_000_000_000:
numeric_timestamp /= 1000
if numeric_timestamp > 0:
return datetime.fromtimestamp(
numeric_timestamp,
tz=MessageBuilder._DATA_TIME_ZONE,
).strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OSError, OverflowError):
pass
return datetime.now(MessageBuilder._DATA_TIME_ZONE).strftime(
"%Y-%m-%d %H:%M:%S"
)
@classmethod
def _ensure_data_time_footer(cls, template: str) -> str:
"""为不使用公共布局的动态 HTML 补上统一的数据时间区域。"""
if cls._DATA_TIME_MARKER in template:
return template
footer = """
"""
body_end = template.lower().rfind("