mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 05:31:52 +00:00
fix(pdf): 删除 PDF 模块和相关的功能
This commit is contained in:
@@ -77,7 +77,7 @@ _✨ 一个基于 AstrBot 的智能群聊分析插件,支持 **QQ (OneBot)**
|
||||
### 📊 可视化报告
|
||||
- **多种格式**: 支持图片和文本输出格式
|
||||
- **精美图片**: 生成美观的可视化报告
|
||||
- **PDF报告**: 生成专业的PDF格式分析报告(需配置)
|
||||
- **HTML报告**: 生成清晰的HTML格式分析报告,可以进一步配置为外链形式发送
|
||||
- **QQ群**: 支持上传到群相册和群文件,查阅黑历史友好
|
||||
- **详细数据**: 包含消息统计、时间分布、关键词、金句等
|
||||
|
||||
|
||||
+1
-26
@@ -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",
|
||||
|
||||
@@ -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 生成过程
|
||||
@@ -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,3 +1,2 @@
|
||||
playwright>=1.40.0
|
||||
diskcache
|
||||
ulid-py
|
||||
@@ -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,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,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">></span> Daily Analysis</h1>
|
||||
<div class="code-block">
|
||||
<span class="keyword">stats</span> = {<br>
|
||||
msg: {{message_count}},<br>
|
||||
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>
|
||||
@@ -1,8 +1,6 @@
|
||||
"""
|
||||
工具函数模块
|
||||
包含PDF处理和通用工具函数
|
||||
包含通用工具函数
|
||||
"""
|
||||
|
||||
from .pdf_utils import PDFInstaller
|
||||
|
||||
__all__ = ["PDFInstaller"]
|
||||
__all__ = []
|
||||
|
||||
@@ -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`。"
|
||||
Reference in New Issue
Block a user