fix(pdf): 删除 PDF 模块和相关的功能

This commit is contained in:
SXP-Simon
2026-04-07 22:17:09 +08:00
parent 98d485dbe6
commit ae38febca2
27 changed files with 19 additions and 4907 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ _✨ 一个基于 AstrBot 的智能群聊分析插件,支持 **QQ (OneBot)**
### 📊 可视化报告
- **多种格式**: 支持图片和文本输出格式
- **精美图片**: 生成美观的可视化报告
- **PDF报告**: 生成专业的PDF格式分析报告(需配置)
- **HTML报告**: 生成清晰的HTML格式分析报告,可以进一步配置为外链形式发送
- **QQ群**: 支持上传到群相册和群文件,查阅黑历史友好
- **详细数据**: 包含消息统计、时间分布、关键词、金句等
+1 -26
View File
@@ -61,7 +61,7 @@
"type": "string",
"description": "输出格式",
"default": "image",
"hint": "分析报告的输出格式:image(图片)、text(文本)、pdf(PDF文件)、html(HTML文件)。使用 PDF 需要额外配置,根据文件中的 PDF_功能说明.md 进行配置"
"hint": "分析报告的输出格式:image(图片)、text(文本)、html(HTML文件)。"
},
"report_template": {
"type": "string",
@@ -350,31 +350,6 @@
}
}
},
"pdf": {
"description": "PDF 设置",
"type": "object",
"hint": "PDF 报告输出相关配置,包括输出目录、浏览器路径和文件名格式",
"items": {
"pdf_output_dir": {
"type": "string",
"description": "PDF输出目录",
"default": "",
"hint": "PDF报告文件的保存目录。留空则自动使用插件数据目录下的 reports 目录。"
},
"browser_path": {
"type": "string",
"description": "自定义浏览器路径",
"default": "",
"hint": "要填写的话请你清楚自己在干什么。自定义浏览器的可执行文件路径(如 Chrome 或 Edge 的 .exe 文件)。提示:如果是在网页后台设置,则直接输入普通路径即可(如 C:\\Program Files\\...);如果是手动编辑 config.json 文件,请务必使用双反斜杠 '\\\\' 分隔路径。"
},
"pdf_filename_format": {
"type": "string",
"description": "PDF文件名格式",
"default": "群聊分析报告_${group_id}_${date}_${ulid}.pdf",
"hint": "PDF文件名格式,支持变量:${group_id}(群号)、${date}(日期)、${ulid}(时间排序ID),支持子目录/层级。"
}
}
},
"html": {
"description": "HTML 设置",
"type": "object",
-90
View File
@@ -1,90 +0,0 @@
# 群日常分析插件 - PDF 功能说明
## 概述
本插件现已支持生成 PDF 格式的群聊分析报告,提供更专业的报告输出格式。PDF 引擎已迁移至 Playwright,提供更好的兼容性。
## 功能特性
- 📄 **PDF 报告生成**: 将群聊分析结果导出为精美的 PDF 文档
- 🎨 **专业排版**: 针对 PDF 打印优化的样式设计
- 📊 **完整内容**: 包含所有分析数据(统计信息、话题分析、用户称号、群聊金句)
- 🔧 **灵活配置**: 可自定义输出目录、文件名格式以及**自定义浏览器路径**
## 安装依赖
使用 PDF 功能前,需要安装 playwright 库。有以下几种安装方式:
### 方式一:使用命令安装(推荐)
在群聊中直接使用安装命令:
```
/安装PDF
```
此命令会自动:
- 检查 playwright 是否已安装
- 如未安装,自动安装 playwright 库
- 检查是否需要下载 Chromium 浏览器(已配置自定义路径则跳过)
- 测试 PDF 功能是否正常
### 方式二:手动安装
```bash
pip install playwright
playwright install chromium
```
## 使用方法
### 1. 设置输出格式
使用新增的 `/设置格式` 命令来切换输出格式:
```
/设置格式 pdf
```
### 2. 生成 PDF 报告
设置为 PDF 格式后,使用常规的分析命令:
```
/群分析
```
### 3. 高级配置:自定义浏览器路径
如果您希望使用系统中已有的 Chrome 或 Edge,或者在特定的路径下运行浏览器,可以在配置中设置 `browser_path`
```json
{
"browser_path": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
}
```
## 故障排除
### 1. Playwright 安装失败
请确保您的系统网络能够访问官网下载浏览器内核。如果是在受限环境下,建议使用系统的 Chrome/Edge 并配置 `browser_path`
### 2. PDF 生成失败
- 检查 `browser_path` 是否正确且可执行。
- 确保系统安装了必要的字体。
## 更新日志
### v1.2.0
- 🚀 **Playwright 迁移**: 替换了不稳定的 pyppeteer。
- 🛠️ **自定义浏览器**: 支持手动指定 Chrome/Edge 路径。
-**安装优化**: 自动检测系统浏览器并减少不必要的下载。
## 技术实现
PDF 生成基于以下技术:
- **Playwright**: 现代无头浏览器控制
- **HTML/CSS**: 报告模板和样式
- **异步处理**: 非阻塞的 PDF 生成过程
+4 -63
View File
@@ -47,7 +47,6 @@ from .src.infrastructure.reporting.generators import ReportGenerator
from .src.infrastructure.scheduler.auto_scheduler import AutoScheduler
from .src.shared.trace_context import TraceContext, TraceLogFilter
from .src.utils.logger import logger
from .src.utils.pdf_utils import PDFInstaller
from .src.utils.resilience import GlobalRateLimiter
@@ -621,21 +620,6 @@ class GroupDailyAnalysis(Star):
await adapter.send_text_report(group_id, text_report)
return
elif output_format == "pdf":
pdf_path = await self.report_generator.generate_pdf_report(
analysis_result,
group_id,
avatar_getter=avatar_url_getter,
nickname_getter=nickname_getter,
)
if pdf_path:
if not await adapter.send_file(group_id, pdf_path):
yield event.chain_result(
[File(name=Path(pdf_path).name, file=pdf_path)]
)
else:
yield event.plain_result("⚠️ PDF 生成失败。")
elif output_format == "html":
html_path, json_path = await self.report_generator.generate_html_report(
analysis_result,
@@ -689,29 +673,19 @@ class GroupDailyAnalysis(Star):
if not format_type:
current_format = self.config_manager.get_output_format()
pdf_status = (
""
if self.config_manager.playwright_available
else "❌ (需安装 Playwright)"
)
yield event.plain_result(f"""📊 当前输出格式: {current_format}
可用格式:
• image - 图片格式 (默认)
• text - 文本格式
• pdf - PDF 格式 {pdf_status}
• html - HTML 格式
用法: /设置格式 [格式名称]""")
return
format_type = format_type.lower()
if format_type not in ["image", "text", "pdf", "html"]:
yield event.plain_result("❌ 无效的格式类型,支持: image, text, pdf, html")
return
if format_type == "pdf" and not self.config_manager.playwright_available:
yield event.plain_result("❌ PDF 格式不可用,请使用 /安装PDF 命令安装依赖")
if format_type not in ["image", "text", "html"]:
yield event.plain_result("❌ 无效的格式类型,支持: image, text, html")
return
self.config_manager.set_output_format(format_type)
@@ -807,37 +781,6 @@ class GroupDailyAnalysis(Star):
)
yield event.chain_result([preview_nodes])
@filter.command("安装PDF", alias={"install_pdf"})
@filter.permission_type(PermissionType.ADMIN)
async def install_pdf_deps(self, event: AstrMessageEvent):
"""
安装 PDF 功能依赖(跨平台支持)
用法: /安装PDF
"""
if self._terminating:
return
current_task = asyncio.current_task()
if current_task:
self._background_tasks.add(current_task)
try:
yield event.plain_result("🔄 开始安装 PDF 功能依赖,请稍候...")
result = await PDFInstaller.install_playwright(
self.config_manager, task_registry=self._background_tasks
)
yield event.plain_result(result)
except asyncio.CancelledError:
logger.info("PDF 安装任务被取消")
except Exception as e:
logger.error(f"安装 PDF 依赖失败: {e}", exc_info=True)
yield event.plain_result(f"❌ 安装过程中出现错误: {str(e)}")
finally:
if current_task:
self._background_tasks.discard(current_task)
@filter.command("分析设置", alias={"analysis_settings"})
@filter.permission_type(PermissionType.ADMIN)
async def analysis_settings(self, event: AstrMessageEvent, action: str = "status"):
@@ -915,7 +858,6 @@ class GroupDailyAnalysis(Star):
)
auto_time = self.config_manager.get_auto_analysis_time()
pdf_status = PDFInstaller.get_pdf_status(self.config_manager)
output_format = self.config_manager.get_output_format()
min_threshold = self.config_manager.get_min_messages_threshold()
@@ -941,12 +883,11 @@ class GroupDailyAnalysis(Star):
• 增量分析: {incremental_status_text}
• 调试模式: {debug_status} (增量立即报告)
• 输出格式: {output_format}
• PDF 功能: {pdf_status}
• 最小消息数: {min_threshold}
💡 可用命令: enable, disable, status, reload, test, incremental_debug
💡 支持的输出格式: image, text, pdf (图片和PDF包含活跃度可视化)
💡 其他命令: /设置格式, /安装PDF, /增量状态""")
💡 支持的输出格式: image, text (图片包含活跃度可视化)
💡 其他命令: /设置格式, /增量状态""")
@filter.command("增量状态", alias={"incremental_status"})
@filter.permission_type(PermissionType.ADMIN)
-1
View File
@@ -1,3 +1,2 @@
playwright>=1.40.0
diskcache
ulid-py
+9 -4
View File
@@ -25,17 +25,22 @@ class IReportGenerator(ABC):
pass
@abstractmethod
async def generate_pdf_report(
async def generate_html_report(
self,
analysis_result: dict,
group_id: str,
avatar_getter: Any = None,
avatar_url_getter: Any = None,
nickname_getter: Any = None,
) -> str | None:
"""生成 PDF 报告"""
) -> tuple[str | None, str | None]:
"""生成 HTML 报告"""
pass
@abstractmethod
def generate_text_report(self, analysis_result: dict) -> str:
"""生成文本报告"""
pass
@abstractmethod
async def close(self):
"""释放资源"""
pass
+1 -136
View File
@@ -1,10 +1,8 @@
"""
配置管理模块 - 基础设施层
负责处理插件配置和PDF依赖检查
负责处理插件配置
"""
import sys
from astrbot.api import AstrBotConfig
from astrbot.api.star import StarTools
@@ -21,15 +19,11 @@ class ConfigManager:
- llm: LLM 设置
- analysis_features: 分析功能开关
- incremental: 增量分析设置
- pdf: PDF 设置
- prompts: 提示词模板
"""
def __init__(self, config: AstrBotConfig):
self.config = config
self._playwright_available = False
self._playwright_version = None
self._check_playwright_availability()
def _get_group(self, group: str) -> dict:
"""获取指定分组的配置字典,不存在时返回空字典"""
@@ -240,26 +234,6 @@ class ConfigManager:
"plugin_specific_persona_id", ""
)
def get_pdf_output_dir(self) -> str:
"""获取PDF输出目录"""
from pathlib import Path
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
try:
default_path = StarTools.get_data_dir() / "reports"
val = self._get_group("pdf").get("pdf_output_dir")
return val if val else str(default_path)
except Exception:
val = self._get_group("pdf").get("pdf_output_dir")
fallback_path = (
Path(get_astrbot_data_path())
/ "plugin_data"
/ "astrbot_plugin_qq_group_daily_analysis"
/ "reports"
)
return val if val else str(fallback_path)
def get_bot_self_ids(self) -> list:
"""获取机器人自身的 ID 列表 (兼容 bot_qq_ids)"""
basic = self._get_group("basic")
@@ -268,12 +242,6 @@ class ConfigManager:
ids = basic.get("bot_qq_ids", [])
return ids
def get_pdf_filename_format(self) -> str:
"""获取PDF文件名格式"""
return self._get_group("pdf").get(
"pdf_filename_format", "群聊分析报告_{group_id}_{date}.pdf"
)
def get_html_output_dir(self) -> str:
"""获取HTML输出目录"""
from pathlib import Path
@@ -407,11 +375,6 @@ class ConfigManager:
)
# 2. 文件名格式升级
modified |= self._upgrade_config_item(
"pdf",
"pdf_filename_format",
self.set_pdf_filename_format,
)
modified |= self._upgrade_config_item(
"html",
"html_filename_format",
@@ -616,16 +579,6 @@ class ConfigManager:
self._ensure_group("analysis_features")["max_golden_quotes"] = count
self.config.save_config()
def set_pdf_output_dir(self, directory: str):
"""设置PDF输出目录"""
self._ensure_group("pdf")["pdf_output_dir"] = directory
self.config.save_config()
def set_pdf_filename_format(self, format_str: str):
"""设置PDF文件名格式"""
self._ensure_group("pdf")["pdf_filename_format"] = format_str
self.config.save_config()
def set_html_filename_format(self, format_str: str):
"""设置HTML文件名格式"""
self._ensure_group("html")["html_filename_format"] = format_str
@@ -754,94 +707,6 @@ class ConfigManager:
"""获取多群增量分析的交错间隔(秒),避免 API 压力"""
return self._get_group("incremental").get("incremental_stagger_seconds", 30)
@property
def playwright_available(self) -> bool:
"""检查playwright是否可用"""
return self._playwright_available
@property
def playwright_version(self) -> str | None:
"""获取playwright版本"""
return self._playwright_version
def _check_playwright_availability(self):
"""检查 playwright 可用性"""
try:
import importlib.util
if importlib.util.find_spec("playwright") is None:
raise ImportError
import playwright
from playwright.async_api import async_playwright # noqa: F401
self._playwright_available = True
try:
self._playwright_version = playwright.__version__
logger.info(f"使用 playwright {self._playwright_version} 作为 PDF 引擎")
except AttributeError:
self._playwright_version = "unknown"
logger.info("使用 playwright (版本未知) 作为 PDF 引擎")
except ImportError:
self._playwright_available = False
self._playwright_version = None
logger.warning(
"playwright 未安装,PDF 功能将不可用。请使用 pip install playwright 安装,并运行 playwright install chromium"
)
def get_browser_path(self) -> str:
"""获取自定义浏览器路径"""
return self._get_group("pdf").get("browser_path", "")
def set_browser_path(self, path: str):
"""设置自定义浏览器路径"""
self._ensure_group("pdf")["browser_path"] = path
self.config.save_config()
def reload_playwright(self) -> bool:
"""重新加载 playwright 模块"""
try:
logger.info("开始重新加载 playwright 模块...")
modules_to_remove = [
mod for mod in sys.modules.keys() if mod.startswith("playwright")
]
logger.info(f"移除模块: {modules_to_remove}")
for mod in modules_to_remove:
del sys.modules[mod]
try:
import playwright
self._playwright_available = True
try:
self._playwright_version = playwright.__version__
logger.info(
f"重新加载成功,playwright 版本: {self._playwright_version}"
)
except AttributeError:
self._playwright_version = "unknown"
logger.info("重新加载成功,playwright 版本未知")
return True
except ImportError:
logger.info("playwright 重新导入可能需要重启 AstrBot")
self._playwright_available = False
self._playwright_version = None
return False
except Exception:
logger.info("playwright 重新导入失败")
self._playwright_available = False
self._playwright_version = None
return False
except Exception as e:
logger.error(f"重新加载 playwright 时出错: {e}")
return False
def save_config(self):
"""保存配置到AstrBot配置系统"""
try:
+1 -1
View File
@@ -1,6 +1,6 @@
"""
报告生成模块
包含HTML、PDF、文本报告生成功能
包含HTML、图片、文本报告生成功能
"""
from .generators import ReportGenerator
@@ -48,8 +48,6 @@ class ReportDispatcher:
success = False
if output_format == "image":
success = await self._dispatch_image(group_id, analysis_result, platform_id)
elif output_format == "pdf":
success = await self._dispatch_pdf(group_id, analysis_result, platform_id)
elif output_format == "html":
success = await self._dispatch_html(group_id, analysis_result, platform_id)
else:
@@ -113,43 +111,6 @@ class ReportDispatcher:
)
return await self._dispatch_text(group_id, analysis_result, platform_id)
async def _dispatch_pdf(
self, group_id: str, analysis_result: dict[str, Any], platform_id: str | None
) -> bool:
trace_id = TraceContext.get()
# 1. 检查 Playwright
if not self.config_manager.playwright_available:
logger.warning(
f"[{trace_id}] Playwright not available, falling back to text."
)
return await self._dispatch_text(group_id, analysis_result, platform_id)
# 2. 生成 PDF
pdf_path = None
try:
pdf_path = await self.report_generator.generate_pdf_report(
analysis_result, group_id
)
except Exception as e:
logger.error(f"[{trace_id}] Failed to generate PDF report: {e}")
# 3. 发送 PDF
if pdf_path:
sent = await self.message_sender.send_file(
group_id,
pdf_path,
caption="📊 每日群聊分析报告已生成:",
platform_id=platform_id,
)
if sent:
return True
# 4. 回退:文本报告
logger.warning(
f"[{trace_id}] PDF dispatch failed, falling back to text report."
)
return await self._dispatch_text(group_id, analysis_result, platform_id)
async def _dispatch_html(
self, group_id: str, analysis_result: dict[str, Any], platform_id: str | None
) -> bool:
-223
View File
@@ -282,61 +282,6 @@ class ReportGenerator(IReportGenerator):
await self._avatar_session.close()
self._avatar_session = None
async def generate_pdf_report(
self,
analysis_result: dict,
group_id: str,
avatar_getter=None,
nickname_getter=None,
) -> str | None:
"""生成PDF格式的分析报告"""
try:
# 确保输出目录存在(使用 asyncio.to_thread 避免阻塞)
output_dir = Path(self.config_manager.get_pdf_output_dir())
await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True)
# 生成文件路径,支持 {group_id}/{date}/{ulid} 自定义子目录
current_date = datetime.now().strftime("%Y%m%d")
pdf_path = self._build_safe_report_path(
output_dir,
self.config_manager.get_pdf_filename_format(),
group_id=group_id,
date=current_date,
)
# 准备渲染数据
render_data = await self._prepare_render_data(
analysis_result,
chart_template="activity_chart_pdf.html",
avatar_url_getter=avatar_getter,
nickname_getter=nickname_getter,
)
logger.info(f"PDF 渲染数据准备完成,包含 {len(render_data)} 个字段")
# 生成 HTML 内容(使用 Jinja2 渲染器以支持逻辑标签)
html_content = self.html_templates.render_template(
"pdf_template.html", **render_data
)
# 检查HTML内容是否有效
if not html_content:
logger.error("PDF报告HTML渲染失败:返回空内容")
return None
logger.info(f"HTML 内容生成完成,长度: {len(html_content)} 字符")
# 转换为 PDF
success = await self._html_to_pdf(html_content, str(pdf_path))
if success:
return str(pdf_path.absolute())
else:
return None
except Exception as e:
logger.error(f"生成 PDF 报告失败: {e}")
return None
async def generate_html_report(
self,
analysis_result: dict,
@@ -920,171 +865,3 @@ class ReportGenerator(IReportGenerator):
logger.debug("头像缓存已关闭")
except Exception as e:
logger.warning(f"关闭头像缓存失败: {e}")
async def _html_to_pdf(self, html_content: str, output_path: str) -> bool:
"""将 HTML 内容转换为 PDF 文件"""
try:
# 动态导入 playwright
try:
from playwright.async_api import async_playwright # type: ignore
except ImportError:
logger.error("playwright 未安装,无法生成 PDF")
logger.info("💡 请尝试运行: pip install playwright")
return False
import os
import sys
logger.info("启动浏览器进行 PDF 转换 (使用 Playwright)")
async with async_playwright() as p:
browser = None
executable_path = None
# 0. 优先检查配置的自定义路径
custom_browser_path = self.config_manager.get_browser_path()
if custom_browser_path:
if Path(custom_browser_path).exists():
logger.info(
f"使用配置的自定义浏览器路径: {custom_browser_path}"
)
executable_path = custom_browser_path
else:
logger.warning(
f"配置的浏览器路径不存在: {custom_browser_path},尝试自动检测..."
)
# 1. 如果没有自定义路径,尝试自动检测系统浏览器
if not executable_path:
system_browser_paths = []
if sys.platform.startswith("win"):
username = os.environ.get("USERNAME", "")
local_app_data = os.environ.get(
"LOCALAPPDATA", rf"C:\Users\{username}\AppData\Local"
)
program_files = os.environ.get(
"ProgramFiles", r"C:\Program Files"
)
program_files_x86 = os.environ.get(
"ProgramFiles(x86)", r"C:\Program Files (x86)"
)
system_browser_paths = [
os.path.join(
program_files, r"Google\Chrome\Application\chrome.exe"
),
os.path.join(
program_files_x86,
r"Google\Chrome\Application\chrome.exe",
),
os.path.join(
local_app_data, r"Google\Chrome\Application\chrome.exe"
),
os.path.join(
program_files_x86,
r"Microsoft\Edge\Application\msedge.exe",
),
os.path.join(
program_files, r"Microsoft\Edge\Application\msedge.exe"
),
]
elif sys.platform.startswith("linux"):
system_browser_paths = [
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
]
elif sys.platform.startswith("darwin"):
system_browser_paths = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
]
# 尝试找到可用的系统浏览器
for path in system_browser_paths:
if Path(path).exists():
executable_path = path
logger.info(f"使用系统浏览器: {path}")
break
# 定义默认启动参数
launch_kwargs = {
"headless": True,
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--font-render-hinting=none",
],
}
if executable_path:
launch_kwargs["executable_path"] = executable_path
launch_kwargs["channel"] = (
"chrome" if "chrome" in executable_path.lower() else "msedge"
)
try:
if executable_path:
# 如果指定了路径,通常使用 chromium 启动
browser = await p.chromium.launch(**launch_kwargs)
else:
# 尝试直接启动,依赖 playwright install
logger.info("尝试启动 Playwright 托管的浏览器...")
browser = await p.chromium.launch(
headless=True, args=launch_kwargs["args"]
)
except Exception as e:
logger.warning(f"浏览器启动失败: {e}")
if "Executable doesn't exist" in str(e) or "executable at" in str(
e
):
logger.error("未找到可用的浏览器。")
logger.info(
"💡 请确保已安装 Playwright 浏览器: playwright install chromium"
)
logger.info("💡 或者安装 Google Chrome / Microsoft Edge")
return False
if not browser:
return False
try:
context = await browser.new_context(device_scale_factor=1)
page = await context.new_page()
# 设置页面内容
await page.set_content(
html_content, wait_until="networkidle", timeout=60000
)
# 生成 PDF
logger.info("开始生成 PDF...")
await page.pdf(
path=output_path,
format="A4",
print_background=True,
margin={
"top": "10mm",
"right": "10mm",
"bottom": "10mm",
"left": "10mm",
},
)
logger.info(f"PDF 生成成功: {output_path}")
return True
except Exception as e:
logger.error(f"PDF 生成过程出错: {e}")
return False
finally:
if browser:
await browser.close()
except Exception as e:
logger.error(f"Playwright 运行出错: {e}")
return False
-23
View File
@@ -91,29 +91,6 @@ class HTMLTemplates:
logger.error(f"加载图片模板失败: {e}")
return ""
async def get_pdf_template_async(self) -> str:
"""获取PDF报告的HTML模板(异步版本,返回原始模板字符串)"""
try:
env = await self._get_env_async()
template = env.get_template("pdf_template.html")
return await asyncio.to_thread(
self._read_template_file_sync, template.filename
)
except Exception as e:
logger.error(f"加载PDF模板失败: {e}")
return ""
def get_pdf_template(self) -> str:
"""获取PDF报告的HTML模板(同步版本,向后兼容)"""
try:
env = self._get_env()
template = env.get_template("pdf_template.html")
with open(template.filename, encoding="utf-8") as f:
return f.read()
except Exception as e:
logger.error(f"加载PDF模板失败: {e}")
return ""
def render_template(self, template_name: str, **kwargs) -> str:
"""渲染指定的模板文件
@@ -1,577 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATRI 群聊日报 · PDF版</title>
<style>
@page {
size: A4;
margin: 0;
}
@font-face {
font-family: 'LXGW WenKai';
src: url('https://c247c050.cloudflare-imgbed-37m.pages.dev/file/1774880718993_LXGWWenKai-Regular.woff2') format('woff2');
font-weight: 400;
}
@font-face {
font-family: 'LXGW WenKai';
src: url('https://c247c050.cloudflare-imgbed-37m.pages.dev/file/1774880715380_LXGWWenKai-Medium.woff2') format('woff2');
font-weight: 600;
}
@font-face {
font-family: 'LXGW WenKai Mono';
src: url('https://c247c050.cloudflare-imgbed-37m.pages.dev/file/1774880717027_LXGWWenKaiMono-Regular.woff2') format('woff2');
font-weight: 400;
}
:root {
--sky: #74c8f3;
--sea: #4ba9dc;
--deep: #27678f;
--foam: #eefaff;
--paper: #fffdf7;
--card: rgba(255, 255, 255, 0.82);
--card-soft: rgba(246, 251, 255, 0.9);
--border: rgba(103, 188, 237, 0.35);
--shadow: rgba(52, 126, 170, 0.14);
--text-main: #214861;
--text-sub: #638399;
--accent: #ffd56e;
--radius-xl: 28px;
--radius-lg: 22px;
--radius-md: 16px;
}
body {
font-family: 'LXGW WenKai', 'Microsoft YaHei', 'Arial', sans-serif;
margin: 0;
padding: 0;
background-color: #dff4ff;
background-image:
linear-gradient(180deg, rgba(255,255,255,0.82), rgba(242,251,255,0.95)),
linear-gradient(rgba(239, 250, 255, 0.72), rgba(239, 250, 255, 0.92)),
radial-gradient(circle at 15% 12%, rgba(255, 255, 255, 0.8), rgba(255,255,255,0) 26%),
linear-gradient(rgba(117, 193, 240, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(117, 193, 240, 0.08) 1px, transparent 1px),
url('https://c247c050.cloudflare-imgbed-37m.pages.dev/file/1774881257527_bg1.webp');
background-size: auto, auto, auto, 26px 26px, 26px 26px, cover;
color: var(--text-main);
line-height: 1.6;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.container {
max-width: 190mm;
margin: 0 auto;
min-height: 297mm;
background: rgba(255,255,255,0.72);
border: 1px solid var(--border);
box-shadow: 0 24px 70px var(--shadow);
padding: 18mm 14mm 16mm;
position: relative;
overflow: hidden;
}
.container::before {
content: "";
position: absolute;
inset: 10mm;
border: 1px dashed rgba(103, 188, 237, 0.28);
border-radius: 18px;
pointer-events: none;
}
.container::after {
content: "ATRI";
position: absolute;
right: -8mm;
top: 6mm;
font-family: 'LXGW WenKai Mono', monospace;
font-size: 44mm;
letter-spacing: 2mm;
color: rgba(75, 169, 220, 0.07);
line-height: 1;
pointer-events: none;
}
.header {
position: relative;
margin-bottom: 10mm;
padding: 10mm 42mm 8mm 8mm;
border-radius: var(--radius-lg);
background:
linear-gradient(180deg, rgba(255,255,255,0.82), rgba(245,251,255,0.68)),
linear-gradient(135deg, rgba(137,205,243,0.08), rgba(255,255,255,0));
border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(75, 148, 196, 0.08);
overflow: hidden;
}
.header::before {
content: "";
position: absolute;
top: 5mm;
left: 8mm;
width: 24mm;
height: 6mm;
border-radius: 4px;
background: linear-gradient(180deg, rgba(255,255,255,0.66), rgba(235,248,255,0.34));
border: 1px solid rgba(170, 214, 239, 0.28);
opacity: 0.86;
}
.header::after {
content: "✦";
position: absolute;
right: 7mm;
top: 6mm;
font-size: 12pt;
color: rgba(92, 173, 217, 0.36);
}
.header h1 {
color: var(--text-main);
font-size: 24pt;
margin: 0;
font-weight: 600;
line-height: 1.25;
}
.header p {
color: var(--text-sub);
font-size: 11pt;
margin: 4mm 0 0 0;
font-family: 'LXGW WenKai Mono', monospace;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4mm;
margin: 0 0 8mm 0;
}
.stat-card {
background: linear-gradient(180deg, rgba(255,255,255,0.78), rgba(248,252,255,0.64));
border-radius: var(--radius-md);
padding: 5mm 3mm;
text-align: center;
box-shadow: 0 8px 24px rgba(74, 145, 188, 0.08);
border: 1px solid var(--border);
}
.stat-value {
font-family: 'LXGW WenKai Mono', monospace;
font-size: 18pt;
font-weight: bold;
color: var(--sea);
margin-bottom: 2mm;
}
.stat-label {
color: var(--text-sub);
font-size: 10pt;
font-weight: 500;
}
.section {
margin: 0 0 8mm 0;
page-break-inside: avoid;
padding: 7mm;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
}
.section::before {
content: "";
position: absolute;
left: 7mm;
top: 4mm;
width: 22mm;
height: 5mm;
border-radius: 4px;
background: linear-gradient(180deg, rgba(255,252,240,0.9), rgba(238,248,255,0.48));
opacity: 0.88;
}
.section-title {
color: var(--text-main);
font-size: 16pt;
margin: 0 0 5mm 0;
padding-bottom: 3mm;
border-bottom: 1px solid rgba(103, 188, 237, 0.28);
font-weight: 600;
}
.chart-container {
background: var(--card-soft);
border-radius: var(--radius-md);
padding: 6mm;
margin: 4mm 0;
border: 1px solid rgba(103, 188, 237, 0.2);
}
.items-list {
list-style: none;
padding: 0;
}
.item {
background: linear-gradient(180deg, rgba(255,255,255,0.86), rgba(246,251,255,0.74));
margin: 4mm 0;
padding: 5mm;
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
}
.item-title {
font-weight: bold;
color: var(--sea);
font-size: 11pt;
margin-bottom: 2mm;
}
.item-content {
color: var(--text-main);
line-height: 1.5;
font-size: 10pt;
}
.hero-summary {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 6mm;
margin-bottom: 8mm;
page-break-inside: avoid;
}
.hero-card,
.peak-card {
position: relative;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
background: var(--card);
padding: 7mm;
overflow: hidden;
}
.hero-card {
min-height: 42mm;
}
.hero-label,
.peak-caption {
display: inline-flex;
align-items: center;
padding: 2mm 4mm;
border-radius: 999px;
background: rgba(116, 200, 243, 0.14);
color: var(--deep);
font-size: 9pt;
letter-spacing: 0.5px;
margin-bottom: 3mm;
}
.hero-title {
margin: 0 0 3mm 0;
font-size: 18pt;
line-height: 1.3;
color: var(--text-main);
}
.hero-desc {
color: var(--text-sub);
font-size: 10.5pt;
line-height: 1.75;
max-width: 100%;
}
.peak-date-subtitle {
font-size: 9pt;
color: var(--text-sub);
margin-bottom: 2mm;
font-family: 'LXGW WenKai Mono', monospace;
}
.peak-time {
font-family: 'LXGW WenKai Mono', monospace;
font-size: 20pt;
color: var(--text-main);
font-weight: 600;
margin-bottom: 3mm;
white-space: nowrap;
}
.peak-note {
font-size: 12pt;
line-height: 1.8;
color: var(--text-main);
}
.token-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4mm;
}
.token-box {
border-radius: 12px;
border: 1px solid rgba(103, 188, 237, 0.2);
background: linear-gradient(180deg, rgba(255,255,255,0.86), rgba(246,251,255,0.74));
padding: 4mm;
text-align: center;
}
.token-box strong {
display: block;
color: var(--sea);
margin-bottom: 2mm;
font-size: 10pt;
}
.quote-wrapper {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 5mm;
position: relative;
page-break-inside: avoid;
}
.q-flex-container {
display: flex;
gap: 3mm;
align-items: flex-start;
}
.q-user-col {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 12mm;
}
.q-avatar-box {
position: relative;
margin-bottom: 1mm;
}
.q-avatar {
width: 12mm;
height: 12mm;
border-radius: 50%;
border: 2px solid #fff;
background: #fff;
object-fit: cover;
box-shadow: 0 4px 10px rgba(91, 160, 201, 0.16);
}
.quote-wrapper:nth-child(odd) .q-avatar {
box-shadow: 0 0 0 1px var(--sky);
}
.quote-wrapper:nth-child(even) .q-avatar {
box-shadow: 0 0 0 1px var(--accent);
}
.q-content-col {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
align-items: flex-start;
}
.q-sender-name {
font-size: 10pt;
color: var(--text-sub);
margin-bottom: 2mm;
margin-left: 2mm;
}
.quote-wrapper:nth-child(even) .q-flex-container {
flex-direction: row-reverse;
}
.quote-wrapper:nth-child(even) .q-content-col {
align-items: flex-end;
}
.quote-wrapper:nth-child(even) .q-sender-name {
margin-left: 0;
margin-right: 2mm;
}
.q-bubble-wrap {
display: flex;
align-items: flex-end;
gap: 2mm;
max-width: 100%;
}
.quote-wrapper:nth-child(even) .q-bubble-wrap {
flex-direction: row-reverse;
}
.q-bubble {
background: rgba(255, 255, 255, 0.95);
border: 1px solid var(--border);
padding: 3mm 4mm;
border-radius: 6mm;
box-shadow: 0 4px 12px rgba(74, 145, 188, 0.08);
position: relative;
width: fit-content;
border-top-left-radius: 1mm;
}
.quote-wrapper:nth-child(even) .q-bubble {
border-top-left-radius: 6mm;
border-top-right-radius: 1mm;
background: rgba(238, 250, 255, 0.95);
}
.q-content {
font-size: 10.5pt;
line-height: 1.6;
color: var(--text-main);
word-break: break-word;
}
.q-analysis-note {
background: rgba(255, 253, 247, 0.9);
padding: 2mm 3mm;
font-size: 9pt;
color: var(--text-sub);
width: fit-content;
max-width: 85%;
margin-top: 2mm;
box-shadow: 0 2px 8px rgba(74, 145, 188, 0.05);
border: 1px solid rgba(117,193,240,0.15);
border-radius: 4mm;
border-top-left-radius: 1mm;
line-height: 1.5;
}
.quote-wrapper:nth-child(even) .q-analysis-note {
background: rgba(255, 249, 230, 0.9);
border-top-left-radius: 4mm;
border-top-right-radius: 1mm;
text-align: right;
}
.note-label {
font-weight: 600;
color: var(--sea);
margin-right: 1mm;
}
.quote-floating-emoji {
display: none;
}
.footer {
text-align: center;
margin-top: 10mm;
padding-top: 6mm;
border-top: 1px solid rgba(103, 188, 237, 0.24);
color: var(--text-sub);
font-size: 9pt;
}
@media print {
body {
background: #dff4ff;
}
.container {
box-shadow: none;
}
.stat-card {
break-inside: avoid;
}
.section {
break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>ATRI 群聊日报 · {{ current_date }}</h1>
<p>{{current_datetime}}</p>
</div>
<div class="hero-summary">
<div class="hero-card">
<div class="hero-label">今日群聊概览</div>
<h2 class="hero-title">今天的聊天像夏日的海风,温柔又热闹。</h2>
<div class="hero-desc">
今天共有 <strong>{{ message_count }}</strong> 条消息,来自 <strong>{{ participant_count }}</strong> 位成员。我已经把这些温暖的对话整理好了,揉进这份属于你们的日报里。
</div>
</div>
<div class="peak-card">
<div class="peak-caption">MOST ACTIVE PERIOD</div>
<div class="peak-date-subtitle">{{ current_datetime }}</div>
<div class="peak-time">{{ most_active_period }}</div>
<div class="peak-note">这段时间的讨论最集中,像潮水推到岸边一样热烈,是今天群聊最明亮的一段波峰。</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{message_count}}</div>
<div class="stat-label">总消息数</div>
</div>
<div class="stat-card">
<div class="stat-value">{{participant_count}}</div>
<div class="stat-label">参与人数</div>
</div>
<div class="stat-card">
<div class="stat-value">{{total_characters}}</div>
<div class="stat-label">总字符数</div>
</div>
<div class="stat-card">
<div class="stat-value">{{emoji_count}}</div>
<div class="stat-label">表情数量</div>
</div>
</div>
<div class="section">
<h2 class="section-title">活跃潮汐</h2>
<div class="chart-container">
{{hourly_chart_html}}
</div>
</div>
<div class="section">
<h2 class="section-title">热门话题</h2>
<ul class="items-list">
{{topics_html | safe}}
</ul>
</div>
<div class="section">
<h2 class="section-title">今日称号</h2>
<ul class="items-list">
{{titles_html | safe}}
</ul>
</div>
<div class="section">
<h2 class="section-title">今日金句</h2>
<div class="html-slot">
{{quotes_html | safe}}
</div>
</div>
<div class="section">
<h2 class="section-title">Token 统计</h2>
<div class="chart-container">
<div class="token-grid">
<div class="token-box">
<strong>总消耗</strong>
{{total_tokens}} tokens
</div>
<div class="token-box">
<strong>提示词 Token</strong>
{{prompt_tokens}} tokens
</div>
<div class="token-box">
<strong>生成内容 Token</strong>
{{completion_tokens}} tokens
</div>
</div>
</div>
</div>
<div class="footer">
<p>Generated by AstrBot Daily Analysis Plugin</p>
<p>ATRI · PDF 模式适配完成</p>
<p style="margin-top: 1mm; display: flex; align-items: center; justify-content: center; gap: 4px; opacity: 0.8; font-size: 8pt;">
<svg viewBox="0 0 24 24" width="12" height="12" fill="var(--text-sub)"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank" style="color: inherit; text-decoration: none; font-family: 'LXGW WenKai Mono', monospace;">SXP-Simon/astrbot_plugin_qq_group_daily_analysis</a>
</p>
<p style="margin-top: 1mm; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 4px;">
<svg viewBox="0 0 24 24" width="12" height="12" fill="var(--text-main)"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5C2 10.79 3.5 12.54 5 14l7 7Z"></path></svg>
ATRI | Template by Liangyu-G
</p>
</div>
</div>
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -1,14 +0,0 @@
{% for item in chart_data %}
<div class="hour-bar-container">
<div class="hour-label">{{ "%02d" | format(item.hour) }}:00</div>
<div class="bar-wrapper">
<div class="bar" style="width: {{ item.percentage }}%;">
{% if item.percentage > 15 %}
<span class="hourly-value-inside">{{ item.count }}</span>
{% endif %}
</div>
{% if item.percentage <= 15 %} <span class="hourly-value-outside">{{ item.count }}</span>
{% endif %}
</div>
</div>
{% endfor %}
@@ -1,563 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>群聊日常分析报告</title>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap"
rel="stylesheet">
<style>
@page {
size: A4;
margin: 0;
}
:root {
--bg-body: #f2f2f2;
--bg-card: #ffffff;
--text-main: #111111;
--text-muted: #666666;
--accent: #222222;
--border: #e0e0e0;
--shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
--radius: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
line-height: 1.6;
padding: 20mm;
-webkit-font-smoothing: antialiased;
}
.container {
width: 100%;
max-width: 170mm;
margin: 0 auto;
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
min-height: 250mm;
}
/* Header - Editorial Style */
.header {
background: #ffffff;
color: var(--text-main);
padding: 50px 30px 30px;
text-align: center;
border-bottom: 1px solid var(--border);
}
.header h1 {
font-family: 'Noto Serif SC', serif;
font-size: 2.4em;
font-weight: 700;
margin-bottom: 12px;
letter-spacing: -0.03em;
color: var(--text-main);
}
.header .date {
font-family: 'Inter', sans-serif;
font-size: 0.9em;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.content {
padding: 40px 30px;
}
.section {
margin-bottom: 50px;
}
.full-width-section {
grid-column: 1 / -1;
}
.section-title {
font-family: 'Noto Serif SC', serif;
font-size: 1.5em;
font-weight: 600;
margin-bottom: 25px;
color: var(--text-main);
position: relative;
padding-left: 16px;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background-color: var(--accent);
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: #fafafa;
padding: 20px 10px;
text-align: center;
border-radius: var(--radius);
border: 1px solid var(--border);
min-width: 0;
/* Prevent overflow */
}
.stat-number {
font-family: 'Inter', sans-serif;
font-size: 1.8em;
font-weight: 600;
color: var(--text-main);
margin-bottom: 6px;
line-height: 1;
letter-spacing: -0.02em;
word-break: break-all;
/* Ensure long numbers wrap if needed */
}
.stat-label {
font-size: 0.75em;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Active Period */
.active-period {
background: var(--accent);
color: #ffffff;
padding: 30px;
text-align: center;
margin: 30px 0;
border-radius: var(--radius);
}
.active-period .time {
font-family: 'Noto Serif SC', serif;
font-size: 2.2em;
font-weight: 400;
margin-bottom: 6px;
}
.active-period .label {
font-size: 0.85em;
opacity: 0.8;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Activity Chart */
.activity-chart-container {
background: #ffffff;
padding: 0;
margin-bottom: 50px;
}
.chart-header {
margin-bottom: 25px;
}
.chart-title {
font-family: 'Noto Serif SC', serif;
font-size: 1.3em;
font-weight: 600;
color: var(--text-main);
}
/* Chart Bars */
.hour-bar-container {
display: flex;
align-items: center;
margin: 10px 0;
height: 18px;
}
.hour-label {
width: 45px;
text-align: left;
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
font-family: 'Inter', sans-serif;
flex-shrink: 0;
}
.bar-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
/* Important for flex child truncation */
}
.bar {
height: 8px;
background: var(--accent);
border-radius: 4px;
opacity: 0.8;
}
.hourly-value-outside {
font-size: 11px;
color: var(--text-muted);
font-weight: 500;
min-width: 25px;
text-align: right;
flex-shrink: 0;
}
.hourly-value-inside {
color: white;
font-size: 10px;
padding: 0 5px;
font-weight: 600;
}
/* Grids */
.topics-grid,
.users-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
align-items: stretch;
}
/* Topic Item Styling */
.topic-item {
background: #fafafa;
padding: 25px;
border-radius: var(--radius);
height: 100%;
border: 1px solid var(--border);
transition: all 0.2s ease;
}
.topic-header {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.topic-number {
background: var(--text-main);
color: #ffffff;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 12px;
font-size: 0.8em;
flex-shrink: 0;
}
.topic-title {
font-family: 'Noto Serif SC', serif;
font-weight: 600;
color: var(--text-main);
font-size: 1.1em;
line-height: 1.3;
}
.topic-contributors {
color: var(--text-muted);
font-size: 0.8em;
margin-bottom: 10px;
padding-left: 36px;
font-style: italic;
}
.topic-detail {
color: #444;
line-height: 1.6;
font-size: 0.9em;
padding-left: 36px;
}
/* User Title Styling */
.user-title {
background: #fafafa;
padding: 20px;
border-radius: var(--radius);
display: flex;
flex-direction: column;
height: 100%;
border: 1px solid var(--border);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.user-avatar,
.user-avatar-placeholder {
width: 44px;
height: 44px;
border-radius: 50%;
margin-right: 15px;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1em;
color: #888;
object-fit: cover;
border: 1px solid rgba(0, 0, 0, 0.05);
flex-shrink: 0;
}
.user-details {
flex: 1;
min-width: 0;
/* Allow text truncation */
}
.user-name {
font-weight: 600;
color: var(--text-main);
font-size: 1em;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-badges {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.user-title-badge {
background: #ffffff;
color: var(--text-main);
border: 1px solid var(--text-main);
padding: 3px 8px;
border-radius: 4px;
font-size: 0.7em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.user-mbti {
background: #e0e0e0;
color: var(--text-main);
padding: 3px 8px;
border-radius: 4px;
font-size: 0.7em;
font-weight: 600;
white-space: nowrap;
}
.user-reason {
color: var(--text-muted);
font-size: 0.85em;
font-weight: 500;
line-height: 1.6;
margin-bottom: 10px;
font-style: italic;
}
/* Quote Item Styling */
.quote-item {
background: #fafafa;
padding: 25px;
margin-bottom: 15px;
border-radius: var(--radius);
border-left: 3px solid var(--text-main);
overflow: hidden;
}
.quote-content {
font-family: 'Noto Serif SC', serif;
font-size: 1.1em;
color: var(--text-main);
font-weight: 500;
line-height: 1.6;
margin-bottom: 10px;
font-style: italic;
}
.quote-author {
font-size: 0.85em;
color: var(--text-main);
font-weight: 600;
text-align: right;
margin-bottom: 6px;
}
.quote-reason {
font-size: 0.75em;
color: var(--text-muted);
background: rgba(0, 0, 0, 0.03);
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
float: right;
clear: both;
}
/* Footer */
.footer {
background: #ffffff;
color: var(--text-muted);
text-align: center;
padding: 30px;
font-size: 0.8em;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.footer a {
color: var(--text-main);
text-decoration: underline;
}
/* Responsive */
@media (max-width: 600px) {
body {
padding: 15px;
}
.content {
padding: 20px;
}
.header {
padding: 40px 20px;
}
.header h1 {
font-size: 1.8em;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.topics-grid,
.users-grid {
grid-template-columns: 1fr;
}
.user-reason {
padding-left: 0;
margin-top: 10px;
}
.stat-number {
font-size: 1.5em;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>群聊日常分析报告</h1>
<div class="date">{{current_date}}</div>
</div>
<div class="content">
<div class="section full-width-section">
<h2 class="section-title">基础统计</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{{message_count}}</div>
<div class="stat-label">消息总数</div>
</div>
<div class="stat-card">
<div class="stat-number">{{participant_count}}</div>
<div class="stat-label">参与人数</div>
</div>
<div class="stat-card">
<div class="stat-number">{{total_characters}}</div>
<div class="stat-label">总字符数</div>
</div>
<div class="stat-card">
<div class="stat-number">{{emoji_count}}</div>
<div class="stat-label">表情数量</div>
</div>
</div>
<div class="active-period">
<div class="time">{{most_active_period}}</div>
<div class="label">最活跃时段</div>
</div>
</div>
<div class="activity-chart-container">
<div class="chart-header">
<div class="chart-title">24小时活跃度分布</div>
</div>
{{hourly_chart_html | safe}}
</div>
{{topics_html | safe}}
{{titles_html | safe}}
{{quotes_html | safe}}
{% if chat_quality_html %}
<div class="section full-width-section">
<h2 class="section-title">智睿洞察 // Chat Quality</h2>
<div style="margin-top: 20px;">
{{ chat_quality_html | safe }}
</div>
</div>
{% endif %}
</div>
<div class="footer">
<div style="text-align: left;">
<strong>Generated by SXP-Simon</strong><br>
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank">
GitHub Repository
</a>
</div>
<div style="text-align: right;">
{{current_datetime}}<br>
Tokens: {{total_tokens}} (P:{{prompt_tokens}} / C:{{completion_tokens}})
</div>
</div>
</div>
</body>
</html>
@@ -1,13 +0,0 @@
{% if chart_data %}
{% for item in chart_data %}
<div class="activity-col">
<span class="activity-val-top">{{ item.count }}</span>
<div class="activity-bar-v" style="--h:{{ "%.2f"|format(item.percentage) }}%;"></div>
<span class="activity-tick">{{ "%02d"|format(item.hour) }}</span>
</div>
{% endfor %}
{% else %}
<div class="activity-col" style="flex:1; justify-content: center; height: 100px;">
<div class="comment">// NO_DATA_STREAM</div>
</div>
{% endif %}
@@ -1,456 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Night Mode Analysis Portal (PDF)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;800&family=Noto+Sans+SC:wght@400;700&display=swap"
rel="stylesheet">
<style>
/* PDF specific styles */
@page {
size: A4;
margin: 0;
}
:root {
--bg-deep: #0a0a0a;
--bg-panel: #141414;
--bg-card: #1c1c1c;
--text-primary: #ffffff;
--text-secondary: #cccccc;
--accent-orange: #ff9900;
--accent-blue: #3399ff;
--accent-red: #ff3366;
--accent-green: #00ff88;
--border-color: #2a2a2a;
--grid-color: rgba(255, 255, 255, 0.03);
--font-mono: 'JetBrains Mono', monospace;
--font-body: 'Inter', 'Noto Sans SC', sans-serif;
}
* {
box-sizing: border-box;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
body {
margin: 0;
background-color: var(--bg-deep);
color: var(--text-primary);
font-family: var(--font-body);
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 30px 30px;
min-height: 100vh;
font-size: 16px;
}
.layout {
width: 100%;
max-width: 190mm;
margin: 0 auto;
padding: 10mm;
min-height: 297mm;
}
.main-container {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
/* Balanced 2-column split */
gap: 30px;
align-items: flex-start;
}
.left-col,
.right-col {
display: flex;
flex-direction: column;
gap: 30px;
}
/* Terminal Window Style */
.window {
background: var(--bg-panel);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
margin-bottom: 0;
}
.window-header {
background: #1e1e1e;
padding: 10px 15px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.traffic-lights {
display: flex;
gap: 8px;
}
.light {
width: 10px;
height: 10px;
border-radius: 50%;
}
.light.red {
background: #ff5f56;
}
.light.green {
background: #27c93f;
}
.window-title {
margin-left: 15px;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text-secondary);
}
.top-nav {
display: flex;
gap: 15px;
margin-bottom: 30px;
font-family: var(--font-mono);
font-size: 0.8rem;
align-items: center;
}
.nav-item {
color: var(--accent-orange);
font-weight: bold;
}
.prompt {
color: var(--accent-orange);
font-weight: bold;
}
.keyword {
color: var(--accent-red);
}
.comment {
color: #888888;
}
h1 {
font-size: 2.8rem;
font-weight: 800;
margin: 0 0 15px;
letter-spacing: -1px;
line-height: 1;
color: #fff;
}
.stat-card {
background: var(--bg-card);
border: 1px solid rgba(255, 153, 0, 0.2);
padding: 20px;
border-radius: 8px;
}
.stat-label {
font-family: var(--font-mono);
color: var(--text-secondary);
font-size: 0.8rem;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 5px;
}
.stat-label::before {
content: '>';
color: var(--accent-orange);
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--accent-orange);
font-family: var(--font-mono);
}
.section-header {
font-family: var(--font-mono);
font-size: 1rem;
margin-bottom: 15px;
color: var(--text-secondary);
}
.code-block {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border-color);
padding: 20px;
border-radius: 6px;
font-family: var(--font-mono);
line-height: 1.5;
font-size: 0.9rem;
}
.chart-container {
height: 220px;
position: relative;
padding: 30px 5px 40px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 2px;
}
.activity-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
min-width: 0;
}
.activity-bar-v {
width: 80%;
max-width: 10px;
background: var(--accent-orange);
height: var(--h, 0%);
border-radius: 1px 1px 0 0;
box-shadow: 0 0 8px rgba(255, 153, 0, 0.3);
position: relative;
}
.activity-tick {
font-family: var(--font-mono);
font-size: 0.5rem;
color: var(--text-secondary);
margin-top: 8px;
transform: rotate(-60deg);
transform-origin: top center;
white-space: nowrap;
}
.activity-val-top {
font-family: var(--font-mono);
font-size: 0.5rem;
color: var(--text-primary);
margin-bottom: 4px;
opacity: 0.6;
}
.topic-list,
.user-grid,
.quote-grid {
display: flex;
flex-direction: column;
gap: 15px;
}
.user-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
padding: 15px;
border-radius: 6px;
display: flex;
gap: 15px;
border-left: 3px solid var(--accent-blue);
}
.user-avatar {
width: 45px;
height: 45px;
border-radius: 6px;
}
.user-info h4 {
margin: 0 0 4px;
font-size: 1rem;
}
.tag {
font-family: var(--font-mono);
font-size: 0.7rem;
padding: 1px 6px;
border-radius: 3px;
border: 1px solid;
margin-right: 5px;
}
.tag.mbti {
color: #ffcc00;
border-color: rgba(255, 204, 0, 0.3);
background: rgba(255, 204, 0, 0.1);
}
.tag.title {
color: var(--accent-blue);
border-color: rgba(51, 153, 255, 0.3);
}
.topic-item {
padding: 15px;
border-left: 3px solid var(--accent-orange);
background: rgba(255, 255, 255, 0.01);
}
.topic-title {
font-weight: bold;
font-size: 1.3rem;
margin-bottom: 8px;
color: var(--accent-orange);
}
.quote-item {
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 6px;
display: flex;
gap: 15px;
align-items: flex-start;
background: #111;
}
.quote-avatar {
width: 35px;
height: 35px;
border-radius: 50%;
border: 1px solid #333;
flex-shrink: 0;
overflow: hidden;
}
.quote-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.quote-body {
flex: 1;
}
.quote-text {
font-style: italic;
font-size: 0.95rem;
color: var(--accent-orange);
margin-bottom: 5px;
line-height: 1.4;
}
footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: flex-end;
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-secondary);
}
.footer-repo a {
color: var(--accent-orange);
text-decoration: underline;
}
.avoid-break {
page-break-inside: avoid;
}
</style>
</head>
<body>
<div class="layout">
<div class="top-nav">
<div class="nav-item">● ready</div>
<div class="nav-item">~/report</div>
</div>
<div class="main-container">
<!-- Left Column: Insights & Topics -->
<div class="left-col">
<div>
<div class="comment">// Report: {{current_date}}</div>
<h1><span class="prompt">&gt;</span> Daily Analysis</h1>
<div class="code-block">
<span class="keyword">stats</span> = {<br>
&nbsp;&nbsp;msg: {{message_count}},<br>
&nbsp;&nbsp;users: {{participant_count}}<br>
}
</div>
</div>
<div>
<div class="section-header"><span class="prompt">$</span> ls ./topics</div>
<div class="topic-list">
{{topics_html | safe}}
</div>
</div>
{% if chat_quality_html %}
<div style="margin-top: 30px;" class="avoid-break">
<div class="section-header"><span class="prompt">$</span> analyze quality</div>
{{ chat_quality_html | safe }}
</div>
{% endif %}
</div>
<!-- Right Column: Chart & People -->
<div class="right-col">
<div class="window avoid-break">
<div class="window-header">
<div class="traffic-lights">
<div class="light red"></div>
<div class="light green"></div>
</div>
<div class="window-title">activity.log</div>
</div>
<div class="chart-container">
{{hourly_chart_html | safe}}
</div>
</div>
<div class="section-header"><span class="prompt">$</span> cat users</div>
<div class="user-grid">
{{titles_html | safe}}
</div>
<div style="margin-top: 30px;">
<div class="section-header"><span class="prompt">$</span> grep golden</div>
<div class="quote-grid">
{{quotes_html | safe}}
</div>
</div>
</div>
</div>
</div>
<footer>
<div class="footer-repo">
<div class="comment">// SXP-Simon</div>
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank">
github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis
</a>
</div>
<div style="text-align: right;">
<div class="comment">// {{current_datetime}}</div>
<div>Tokens: {{total_tokens}} [P:{{prompt_tokens}} C:{{completion_tokens}}]</div>
</div>
</footer>
</div>
</body>
</html>
@@ -1,15 +0,0 @@
{% if chart_data %}
<div class="activity-container">
{% for item in chart_data %}
<div class="activity-col">
<span class="activity-col-count">{{ item.count }}</span>
<div class="activity-bar-vertical" style="--h: {{ "%.2f"|format(item.percentage) }}%;"></div>
<span class="activity-label-hour">{{ "%02d"|format(item.hour) }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="activity-col" style="flex:1; justify-content: center; height: 100px;">
<div class="topic-meta">NO_DATA_AVAILABLE_FOR_ACTIVITY_SPECTRUM</div>
</div>
{% endif %}
@@ -1,562 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Retro Futurism Daily Export (PDF)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;600&family=JetBrains+Mono:wght@400;600&family=Noto+Sans+SC:wght@400;500&display=swap"
rel="stylesheet">
<style>
@font-face {
font-family: "Oswald-Fallback";
src: local("Impact"), local("Arial Black"), local("Helvetica Neue Bold");
font-weight: 400 700;
}
@page {
size: A4;
margin: 0;
}
:root {
/* Faithful Palette - Industrial Retro */
--c-bg: #F8F4E8;
--c-text: #1A1A1A;
--c-accent: #D35400;
--c-accent-glow: rgba(211, 84, 0, 0.4);
--c-dec-1: #2E4053;
--c-dec-2: #A93226;
--c-dec-3: #D4AC0D;
--c-grid: rgba(26, 26, 26, 0.08);
--font-display: "Oswald", "Oswald-Fallback", Impact, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--font-body: "Noto Sans SC", sans-serif;
}
* {
box-sizing: border-box;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
body {
margin: 0;
background-color: var(--c-bg);
color: var(--c-text);
font-family: var(--font-body);
background-image:
linear-gradient(var(--c-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--c-grid) 1px, transparent 1px);
background-size: 15mm 15mm;
min-height: 100vh;
}
.layout {
width: 100%;
max-width: 190mm;
margin: 0 auto;
padding: 10mm;
position: relative;
background: var(--c-bg);
border-right: 3mm solid var(--c-text);
border-bottom: 3mm solid var(--c-text);
min-height: 297mm;
}
/* CRT Console Aesthetic */
.crt-console {
background: #111;
color: #00FF41;
padding: 20px;
font-family: var(--font-mono);
border: 4px solid var(--c-text);
border-radius: 4px;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 1);
position: relative;
overflow: hidden;
margin-bottom: 30px;
}
.crt-header {
display: flex;
gap: 10px;
margin-bottom: 15px;
opacity: 0.7;
}
.crt-btn {
width: 8px;
height: 8px;
background: #333;
border: 1px solid #555;
}
.crt-title {
color: #00FF41;
text-shadow: 0 0 5px #00FF41;
font-size: 0.7rem;
letter-spacing: 0.3rem;
text-transform: uppercase;
}
.crt-value {
font-family: var(--font-display);
font-size: 3rem;
color: #fff;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
margin: 10px 0;
line-height: 1;
}
.crt-sub {
opacity: 0.6;
font-size: 0.7rem;
border-top: 1px solid #333;
padding-top: 10px;
display: flex;
justify-content: space-between;
}
/* Header Scaling */
header {
margin-bottom: 40px;
border-bottom: 2px double var(--c-text);
padding-bottom: 20px;
}
h1 {
font-family: var(--font-display);
font-size: 4rem;
line-height: 0.9;
margin: 0 0 15px;
text-transform: uppercase;
letter-spacing: -0.1rem;
color: var(--c-text);
}
h1 .highlight {
color: var(--c-accent);
}
/* Stats Scaling */
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 30px;
}
.stat-box {
border: 2px solid var(--c-text);
padding: 15px;
background: #fff;
box-shadow: 4px 4px 0 var(--c-text);
}
.stat-label {
font-family: var(--font-mono);
font-size: 0.6rem;
font-weight: bold;
text-transform: uppercase;
color: var(--c-dec-1);
margin-bottom: 8px;
letter-spacing: 0.1rem;
}
.stat-value {
font-family: var(--font-display);
font-size: 2.2rem;
color: var(--c-accent);
line-height: 1;
}
/* Activity Chart - Faithful Restore */
.activity-container {
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 180px;
padding: 10px 0;
border-bottom: 2px solid var(--c-text);
margin-bottom: 20px;
background: #fff;
padding: 10px;
border: 2px solid var(--c-text);
}
.activity-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
gap: 8px;
}
.activity-bar-vertical {
width: 60%;
background: var(--c-accent);
border: 1px solid var(--c-text);
height: var(--h, 0%);
min-height: 2px;
}
.activity-label-hour {
font-family: var(--font-mono);
font-size: 0.55rem;
font-weight: bold;
transform: rotate(-45deg);
white-space: nowrap;
color: var(--c-dec-1);
}
.activity-col-count {
font-family: var(--font-mono);
font-size: 0.6rem;
color: var(--c-text);
}
/* Topics & Sections */
.section-label {
font-family: var(--font-display);
font-size: 2rem;
text-transform: uppercase;
border-bottom: 4px solid var(--c-text);
padding-bottom: 8px;
margin: 30px 0 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-label span {
background: var(--c-text);
color: var(--c-bg);
padding: 4px 10px;
font-size: 0.7rem;
font-family: var(--font-mono);
}
.topic-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.topic-item {
display: flex;
border: 2px solid var(--c-text);
background: #fff;
box-shadow: 6px 6px 0 var(--c-text);
padding: 15px;
position: relative;
}
.topic-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 8px;
background: repeating-linear-gradient(45deg, var(--c-dec-2), var(--c-dec-2) 5px, transparent 5px, transparent 10px);
}
.topic-content {
margin-left: 15px;
flex: 1;
}
.topic-title {
font-family: var(--font-display);
font-size: 1.8rem;
margin: 0 0 10px;
text-transform: uppercase;
}
.topic-meta {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--c-dec-1);
margin-bottom: 10px;
background: var(--c-grid);
padding: 4px 8px;
display: inline-block;
}
.topic-detail {
font-size: 1rem;
line-height: 1.6;
color: #333;
}
/* User Grid */
.title-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.title-item {
display: flex;
gap: 15px;
align-items: flex-start;
padding: 15px;
border: 1.5px solid var(--c-text);
background: #fff;
box-shadow: 4px 4px 0 var(--c-grid);
}
.title-avatar-wrap {
width: 60px;
height: 60px;
flex-shrink: 0;
border: 2px solid var(--c-text);
box-shadow: 3px 3px 0 var(--c-dec-3);
overflow: hidden;
background: #eee;
}
.title-avatar-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(100%);
}
.title-info h4 {
margin: 0 0 5px;
font-family: var(--font-display);
font-size: 1.4rem;
line-height: 1;
}
.title-badge {
font-family: var(--font-mono);
font-size: 0.6rem;
background: var(--c-text);
color: #fff;
padding: 2px 6px;
margin-bottom: 8px;
display: inline-flex;
gap: 5px;
}
.title-reason {
font-size: 0.8rem;
line-height: 1.4;
color: #444;
}
/* Quotes */
.quote-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.quote-item {
display: flex;
gap: 15px;
align-items: flex-start;
padding: 15px;
background: #fff;
border: 2px solid var(--c-text);
border-left: 10px solid var(--c-accent);
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.05);
}
.quote-avatar {
width: 50px;
height: 50px;
flex-shrink: 0;
border: 1.5px solid var(--c-text);
border-radius: 50%;
overflow: hidden;
filter: grayscale(100%);
}
.quote-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.quote-body {
flex: 1;
}
.quote-content {
font-family: var(--font-mono);
font-size: 1rem;
line-height: 1.4;
margin-bottom: 10px;
font-style: italic;
}
.quote-author {
font-weight: bold;
font-family: var(--font-display);
font-size: 1rem;
text-transform: uppercase;
color: var(--c-dec-1);
}
/* Footer Parity */
footer {
margin-top: 40px;
padding-top: 20px;
border-top: 5px solid var(--c-text);
font-family: var(--font-mono);
font-size: 0.65rem;
display: flex;
flex-direction: column;
gap: 12px;
}
.footer-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-tokens {
display: flex;
gap: 20px;
opacity: 0.7;
}
.footer-repo a {
color: var(--c-accent);
text-decoration: underline;
}
</style>
</head>
<body>
<div class="layout">
<header>
<div class="header-meta">System Report // {{current_date}}</div>
<h1>Daily<br><span class="highlight">Analysis</span><br>Export</h1>
<p class="header-brief">
Activity analysis report, generated based on message statistics and AI analysis
</p>
<div class="footer-row">
<div class="meta-block">
<div class="meta-label">Generated At</div>
<span class="meta-value">{{current_datetime}}</span>
</div>
<div class="meta-block">
<div class="meta-label" style="border-left: 2px solid var(--c-dec-2); padding-left: 10px; margin-left: 20px;">Peak Window</div>
<span class="meta-value" style="padding-left: 32px;">{{most_active_period}}</span>
</div>
</div>
</header>
<div class="grid-container">
<div class="stat-box">
<span class="stat-label">Messages</span>
<div class="stat-value">{{message_count}}</div>
</div>
<div class="stat-box">
<span class="stat-label">Users</span>
<div class="stat-value">{{participant_count}}</div>
</div>
<div class="stat-box">
<span class="stat-label">Chars</span>
<div class="stat-value">{{total_characters}}</div>
</div>
<div class="stat-box">
<span class="stat-label">Emoji</span>
<div class="stat-value">{{emoji_count}}</div>
</div>
</div>
<div class="crt-console">
<div class="crt-header">
<span class="crt-btn"></span>
<span class="crt-btn"></span>
<span class="crt-btn"></span>
<div class="crt-title">STABILITY_METRICS.LOG</div>
</div>
<div class="crt-label">PEAK WINDOW SCAN</div>
<div class="crt-value">{{most_active_period}}</div>
<div class="crt-sub">
<span>INTENSITY: {{message_count}} MSGS/DAY</span>
<span>STATUS: OPERATIONAL</span>
</div>
</div>
<div class="section-label">
Hourly Broadcast <span>ACTIVITY_SPECTRUM</span>
</div>
<div style="margin-bottom: 30px;">
{{hourly_chart_html|safe}}
</div>
{% if topics_html %}
<div class="section-label">
Thread Matrix <span>TOPICS_MODULE</span>
</div>
<div class="topic-list">
{{topics_html|safe}}
</div>
{% endif %}
{% if titles_html %}
<div class="section-label" style="margin-top: 40px;">
Operator Registry <span>TITLES_MODULE</span>
</div>
<div class="title-grid">
{{titles_html|safe}}
</div>
{% endif %}
{% if quotes_html %}
<div class="section-label" style="margin-top: 40px;">
Golden Lines <span>QUOTES_MODULE</span>
</div>
<div class="quote-list">
{{quotes_html|safe}}
</div>
{% endif %}
{% if chat_quality_html %}
<div class="section-label" style="margin-top: 40px;">
Quality Analysis <span>STABILITY_METRICS</span>
</div>
<div class="grid-container">
<div style="grid-column: span 12;">
{{ chat_quality_html|safe }}
</div>
</div>
{% endif %}
<footer>
<div class="footer-row">
<div class="footer-repo">
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank">
github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis
</a>
</div>
<div class="footer-author">Generated by // SXP-Simon</div>
</div>
<div class="footer-row">
<div class="footer-tokens">
<div>Tokens: {{total_tokens}}</div>
<div>P/C: {{prompt_tokens}}/{{completion_tokens}}</div>
</div>
<div>{{current_datetime}} // END_OF_LINE_</div>
</div>
</footer>
</div>
</body>
</html>
@@ -1,34 +0,0 @@
<div class="chart-section-horizontal">
{% for item in chart_data %}
{% set bar_bg = 'var(--color-purple)' %}
{% set bar_height = '4px' %}
{% set bar_opacity = '1' %}
{% if item.count == 0 %}
{% set bar_bg = 'var(--ink-secondary)' %}
{% set bar_height = '4px' %}
{% set bar_opacity = '0.2' %}
{% elif item.percentage >= 70 %}
{% set bar_bg = 'var(--accent-orange)' %}
{% set bar_height = item.percentage ~ '%' %}
{% elif item.percentage >= 30 %}
{% set bar_bg = 'var(--color-green)' %}
{% set bar_height = item.percentage ~ '%' %}
{% else %}
{% set bar_bg = 'var(--color-blue)' %}
{% set bar_height = item.percentage ~ '%' %}
{% endif %}
<div class="chart-column">
{% if item.count > 0 %}
<div class="chart-value-top">{{ item.count }}</div>
{% endif %}
{% set style_str = 'height: ' ~ bar_height ~ '; background: ' ~ bar_bg ~ '; opacity: ' ~ bar_opacity ~ ';
border-bottom: none;' %}
<div class="chart-bar-vertical" style="{{ style_str }}"></div>
<div class="chart-label-xaxis">{{ "%02d" | format(item.hour) }}</div>
</div>
{% endfor %}
</div>
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
<div style="display: flex; flex-direction: column; gap: 5px;">
{% for item in chart_data %}
<div style="display: flex; align-items: center;">
<span style="width: 50px;">{{ "%02d" | format(item.hour) }}:00</span>
<div style="background-color: #eee; flex-grow: 1; height: 10px;">
<div style="width: {{ item.percentage }}%; background-color: blue; height: 100%;"></div>
</div>
<span style="width: 30px; text-align: right;">{{ item.count }}</span>
</div>
{% endfor %}
</div>
@@ -1,110 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Simple Report</title>
<style>
@page {
size: A4;
margin: 0;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background: #fafafa;
margin: 0;
padding: 20mm;
color: #333;
}
.container {
max-width: 170mm;
margin: 0 auto;
background: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
min-height: 250mm;
}
.section {
margin-bottom: 25px;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
h1 { color: #2c3e50; border-left: 5px solid #3498db; padding-left: 15px; }
h2 { color: #34495e; font-size: 1.2rem; }
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 2px solid #eee;
color: #7f8c8d;
font-size: 0.85rem;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.footer a { color: #3498db; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h1>群聊日报 - {{current_date}}</h1>
<div class="section">
<h2>基础统计</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
<p>消息数: <strong>{{message_count}}</strong></p>
<p>参与人数: <strong>{{participant_count}}</strong></p>
<p>总字符数: <strong>{{total_characters}}</strong></p>
<p>表情数: <strong>{{emoji_count}}</strong></p>
<p>最活跃时段: <strong>{{most_active_period}}</strong></p>
</div>
</div>
<div class="section">
<h2>活跃度图表</h2>
<div style="background: #f9f9f9; padding: 10px; border-radius: 4px;">
{{hourly_chart_html | safe}}
</div>
</div>
<div class="section">
{{topics_html | safe}}
</div>
<div class="section">
{{titles_html | safe}}
</div>
<div class="section" style="border: none;">
{{quotes_html | safe}}
</div>
{% if chat_quality_html %}
<div class="section">
<h2>Chat Quality</h2>
{{ chat_quality_html | safe }}
</div>
{% endif %}
<div class="footer">
<div>
<strong>Generated by SXP-Simon</strong><br>
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank">
GitHub Repository
</a>
</div>
<div style="text-align: right;">
{{current_datetime}}<br>
Tokens: {{total_tokens}} (P:{{prompt_tokens}} / C:{{completion_tokens}})
</div>
</div>
</div>
</body>
</html>
@@ -1,44 +0,0 @@
{% if chart_data %}
<div class="sf-chart-container"
style="display: flex; height: 260px; align-items: flex-end; justify-content: space-around; width: 100%; padding-bottom: 40px; box-sizing: border-box;">
{% for item in chart_data %}
<div class="sf-chart-col"
style="flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; position: relative;">
<!-- 数值显示 -->
<div class="sf-chart-val"
style="font-size: 10px; font-weight: bold; color: #b71c1c; margin-bottom: 4px; font-family: sans-serif;">
{{ item.count if item.count > 0 else '' }}
</div>
<!-- 图表柱体 -->
<div class="sf-chart-bar" style="
width: 70%;
height: {{ item.percentage }}%;
background: linear-gradient(to top, #b71c1c, #ffca28);
border-radius: 4px 4px 0 0;
box-shadow: 0 0 12px rgba(183, 28, 28, 0.3);
border: 1px solid rgba(255, 202, 40, 0.4);
min-height: {{ '2px' if item.count > 0 else '0px' }};
"></div>
<!-- 刻度文字 -->
<div class="sf-chart-tick" style="
position: absolute;
bottom: -25px;
font-size: 10px;
color: #3e2723;
white-space: nowrap;
transform: rotate(-45deg);
font-weight: bold;
">
{{ "%02d"|format(item.hour) }}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div
style="display:flex; align-items:center; justify-content:center; height: 200px; width: 100%; color: #888; font-style: italic;">
🏮 尚无活跃记录 🏮
</div>
{% endif %}
@@ -1,352 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>春节特供日报 (PDF)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=ZCOOL+XiaoWei&family=Noto+Serif+SC:wght@400;700&display=swap"
rel="stylesheet">
<style>
@page {
size: A4;
margin: 0;
}
:root {
--red-festive: #b71c1c;
--gold-primary: #ffca28;
--wood-dark: #3e2723;
--bg-paper: #fffcf0;
--text-main: #3e2723;
--font-calligraphy: 'Ma Shan Zheng', cursive;
--font-heading: 'ZCOOL XiaoWei', serif;
--font-body: 'Noto Serif SC', serif;
}
* {
box-sizing: border-box;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
body {
margin: 0;
background-color: var(--bg-paper);
color: var(--text-main);
font-family: var(--font-body);
font-size: 14px;
}
.layout {
width: 100%;
max-width: 190mm;
margin: 0 auto;
padding: 10mm;
border: 10mm solid var(--red-festive);
min-height: 297mm;
position: relative;
}
/* Decoration */
.decoration {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
border: 2px solid var(--gold-primary);
pointer-events: none;
}
.sf-header {
text-align: center;
margin-bottom: 40px;
border-bottom: 5px double var(--red-festive);
padding-bottom: 20px;
}
.sf-header h1 {
font-family: var(--font-calligraphy);
font-size: 4rem;
color: var(--red-festive);
margin: 0;
}
.sf-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.sf-hongbao {
background: var(--red-festive);
padding: 15px;
border-radius: 8px;
color: var(--gold-primary);
text-align: center;
}
.sf-badge {
font-family: var(--font-heading);
font-size: 0.75rem;
font-weight: bold;
padding: 1px 8px;
border: 2px solid;
position: relative;
line-height: 1;
}
.sf-badge.mbti {
color: var(--red-festive);
border-color: var(--red-festive);
background: transparent;
}
.sf-badge.sf-title {
color: #fff;
background: var(--red-festive);
border-color: var(--red-festive);
}
.hb-label {
font-size: 0.8rem;
margin-bottom: 5px;
}
.hb-value {
font-size: 1.8rem;
font-weight: bold;
}
.sf-main {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 25px;
}
.sf-section-title {
font-family: var(--font-calligraphy);
font-size: 2rem;
color: var(--red-festive);
border-left: 5px solid var(--red-festive);
padding-left: 10px;
margin-bottom: 15px;
margin-top: 25px;
}
.sf-topic-card {
background: #fff;
padding: 15px;
margin-bottom: 15px;
border: 1px solid var(--red-festive);
}
.sf-topic-title {
font-weight: bold;
font-size: 1.2rem;
color: var(--red-festive);
margin-bottom: 5px;
}
.sf-topic-meta {
font-size: 0.8rem;
color: #666;
margin-bottom: 10px;
}
/* User Grid */
.sf-user-card {
background: #fff;
border: 1px solid var(--red-festive);
outline: 1px solid var(--red-festive);
outline-offset: 3px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
margin-bottom: 20px;
}
.sf-card-header {
display: flex;
align-items: center;
gap: 15px;
border-bottom: 1px dashed rgba(183, 28, 28, 0.2);
padding-bottom: 10px;
}
.sf-avatar-wrap {
width: 50px;
height: 50px;
flex-shrink: 0;
border: 2px solid var(--gold-primary);
overflow: hidden;
background: var(--red-festive);
padding: 2px;
border-radius: 50%;
}
.sf-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.sf-user-name {
font-weight: bold;
color: var(--red-festive);
margin: 0 0 5px;
font-size: 1.1rem;
}
.sf-user-reason {
font-size: 0.95rem;
line-height: 1.5;
text-align: justify;
}
.sf-quote-item {
padding: 15px;
border: 2px solid var(--red-festive);
margin-bottom: 15px;
background: #fff;
}
.sf-quote-content {
font-family: var(--font-heading);
font-size: 1.3rem;
margin-bottom: 10px;
}
.sf-quote-author {
text-align: right;
font-weight: bold;
color: var(--red-festive);
}
.chart-container {
height: 180px;
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 10px;
border: 1px solid #ddd;
background: #fff;
}
.sf-chart-col {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
flex: 1;
}
.sf-chart-bar {
width: 60%;
background: var(--red-festive);
height: var(--h);
}
.sf-chart-tick {
font-size: 0.5rem;
transform: rotate(-45deg);
}
footer {
margin-top: 40px;
text-align: center;
border-top: 1px solid var(--gold-primary);
padding-top: 20px;
font-size: 0.8rem;
color: var(--red-festive);
display: flex;
flex-direction: column;
gap: 8px;
}
.footer-repo a {
color: var(--red-festive);
text-decoration: underline;
}
.avoid-break {
page-break-inside: avoid;
}
</style>
</head>
<body>
<div class="layout">
<div class="decoration"></div>
<div class="sf-header">
<h1>🏮 春节特供日报 🏮</h1>
<div>—— {{current_date}} ——</div>
</div>
<div class="sf-stats-grid">
<div class="sf-hongbao">
<div class="hb-label">消息数</div>
<div class="hb-value">{{message_count}}</div>
</div>
<div class="sf-hongbao">
<div class="hb-label">群友数</div>
<div class="hb-value">{{participant_count}}</div>
</div>
<div class="sf-hongbao">
<div class="hb-label">总字数</div>
<div class="hb-value">{{total_characters}}</div>
</div>
<div class="sf-hongbao">
<div class="hb-label">活跃窗口</div>
<div class="hb-value" style="font-size: 1.2rem;">{{most_active_period}}</div>
</div>
</div>
<div class="sf-main">
<div class="left-col">
<div class="sf-section-title">核心话题回顾</div>
{{topics_html | safe}}
</div>
<div class="right-col">
<div class="sf-section-title">活跃趋势</div>
<div class="chart-container">
{{hourly_chart_html | safe}}
</div>
<div class="sf-section-title">群友风云榜</div>
{{titles_html | safe}}
</div>
</div>
{% if chat_quality_html %}
<div class="sf-section-title">智睿点评 // Chat Quality</div>
{{ chat_quality_html | safe }}
{% endif %}
<div class="sf-section-title">每日金句回响</div>
{{quotes_html | safe}}
<footer>
<div class="footer-repo">
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank">
github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis
</a>
</div>
<div>Generated by SXP-Simon // {{current_datetime}}</div>
<div style="font-size: 0.75rem; opacity: 0.8;">
Tokens: {{total_tokens}} (P: {{prompt_tokens}} / C: {{completion_tokens}})
</div>
</footer>
</div>
</body>
</html>
+2 -4
View File
@@ -1,8 +1,6 @@
"""
工具函数模块
包含PDF处理和通用工具函数
包含通用工具函数
"""
from .pdf_utils import PDFInstaller
__all__ = ["PDFInstaller"]
__all__ = []
-191
View File
@@ -1,191 +0,0 @@
"""
PDF工具模块
负责PDF相关的安装和管理功能
"""
import asyncio
import sys
from pathlib import Path
from typing import Any
from .logger import logger
class PDFInstaller:
"""
工具组件:PDF 渲染引擎 (Playwright) 安装器
该组件负责管理 Playwright 及其对应浏览器内核 (Chromium) 的安装生命周期。
由于内核下载耗时较长且受网络波动影响,采用非阻塞的后台任务模式执行。
"""
# 静态安装状态追踪
_install_status: dict[str, Any] = {
"in_progress": False,
"completed": False,
"failed": False,
"error_message": None,
}
@staticmethod
async def install_playwright(
config_manager: Any, task_registry: set[asyncio.Task] | None = None
) -> str:
"""
异步入口:安装 Playwright 环境。
流程:
1. 调用 pip 安装 `playwright` Python 包。
2. 验证自定义浏览器路径配置。
3. 若无自定义路径,则触发浏览器内核安装。
Args:
config_manager (Any): 配置管理实例,用于读取/设置安装状态。
Returns:
str: 安装阶段提示信息
"""
try:
logger.info("正在初始化 Playwright 安装流程...")
# 1. 下载并安装库文件
logger.info("第一步:正在运行 pip install playwright...")
process = await asyncio.create_subprocess_exec(
sys.executable,
"-m",
"pip",
"install",
"playwright>=1.40.0",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
logger.error(f"Playwright 库安装失败: {error_msg}")
return f"❌ pip install playwright 失败: {error_msg}"
logger.info("第一步完成。正在检查浏览器内核...")
custom_path = config_manager.get_browser_path()
if custom_path and Path(custom_path).exists():
logger.info(f"检测到自定义浏览器路径: {custom_path}。跳过内核下载。")
return f"✅ Playwright 库已就绪。已检测到自定义浏览器 `{custom_path}`,无需额外安装内核。您可以直接开始生成 PDF。"
# 3. 部署浏览器内核
return await PDFInstaller.install_system_deps(task_registry)
except Exception as e:
logger.error(f"Playwright 设置过程中出错: {e}")
return f"❌ 安装过程中出错: {str(e)}"
@staticmethod
async def install_system_deps(
task_registry: set[asyncio.Task] | None = None,
) -> str:
"""
触发浏览器内核的后台异步安装流程。
该方法检查防重入状态,并立即返回任务启动信息,不会阻塞主线程。
Returns:
str: 任务排队状态提示
"""
try:
if PDFInstaller._install_status["in_progress"]:
return "⏳ 浏览器内核正在后台部署中,请稍后检查日志或状态。"
PDFInstaller._install_status.update(
{
"in_progress": True,
"completed": False,
"failed": False,
"error_message": None,
}
)
logger.info("正在启动后台线程以部署 Chromium 内核...")
task = asyncio.create_task(PDFInstaller._background_playwright_install())
if task_registry is not None:
task_registry.add(task)
task.add_done_callback(task_registry.discard)
return (
"🚀 浏览器内核安装任务已成功在后台启动。\n\n"
"程序正在执行 `playwright install chromium`,由于体积较大,通常需花费 2-5 分钟。\n"
"此过程不会影响机器人正常响应。安装完成后,系统日志将进行通知。"
)
except Exception as e:
PDFInstaller._install_status["in_progress"] = False
logger.error(f"启动安装任务失败: {e}")
return f"❌ 启动安装任务失败: {e}"
@staticmethod
async def _background_playwright_install() -> None:
"""
底层宿主任务:驱动 system shell 执行浏览器二进制文件部署。
"""
try:
logger.info("正在执行二进制文件:playwright install chromium")
# 通过当前 Python 解释器环境调用子模块,确保环境隔离
process = await asyncio.create_subprocess_exec(
sys.executable,
"-m",
"playwright",
"install",
"chromium",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
PDFInstaller._install_status["completed"] = True
logger.info("✅ Chromium 内核安装成功。")
# Linux 特殊处理:提示用户补充系统依赖
if sys.platform.startswith("linux"):
logger.info(
"提示:在 Linux 上,如果 PDF 生成仍然失败,请尝试运行 'sudo playwright install-deps'"
)
else:
PDFInstaller._install_status["failed"] = True
PDFInstaller._install_status["error_message"] = stderr.decode().strip()
logger.error(f"❌ Chromium 安装二进制文件执行失败: {stderr.decode()}")
except Exception as e:
PDFInstaller._install_status.update(
{"failed": True, "error_message": str(e)}
)
logger.error(f"Playwright 后台任务遇到异常: {e}")
finally:
PDFInstaller._install_status["in_progress"] = False
@staticmethod
def get_pdf_status(config_manager: Any) -> str:
"""
查询当前系统的 PDF 功能可用性状态描述。
Args:
config_manager (Any): 配置管理器,用于读取核心探测开关。
Returns:
str: 用户友好的状态文本
"""
if getattr(config_manager, "playwright_available", False):
version = getattr(config_manager, "playwright_version", "Unknown")
status = f"✅ PDF 功能可用 (核心版本: {version})"
if PDFInstaller._install_status["in_progress"]:
status += "\n⏳ 警告:浏览器内核仍在后台下载/部署中..."
elif PDFInstaller._install_status["failed"]:
status += f"\n⚠️ 上次内核安装异常: {PDFInstaller._install_status.get('error_message')}"
return status
else:
return "❌ PDF 渲染核心未安装 - 请发送管理员指令 `/安装PDF`。"