feat(html): Add HTML output format support (#143 @lekoOwO)

* feat: add HTML output format support

- Add HTML settings section to _conf_schema.json with base URL and output directory configuration
- Add HTML configuration methods to ConfigManager (get_html_output_dir, get_html_base_url, get_html_filename_format)
- Create html_template.html files for all themes (fallback to image_template.html)
- Implement generate_html_report method in ReportGenerator to save HTML and JSON files
- Update main.py to handle HTML output format and send both file and URL if base_url is configured
- Update set_output_format command to include HTML as a valid format option

Co-authored-by: lekoOwO <20151124+lekoOwO@users.noreply.github.com>

* feat: enhance JSON serialization in HTML report generation

* fix: 优化输出目录获取逻辑

---------

Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: lekoOwO <20151124+lekoOwO@users.noreply.github.com>
Co-authored-by: SXP-Simon <sxp20061207@163.com>
This commit is contained in:
leko
2026-03-31 20:06:57 +08:00
committed by GitHub
co-authored by lekoOwO anthropic-code-agent[bot] SXP-Simon
parent 697213826c
commit 3662ac3d75
13 changed files with 4820 additions and 39 deletions
+29 -4
View File
@@ -61,7 +61,7 @@
"type": "string",
"description": "输出格式",
"default": "image",
"hint": "分析报告的输出格式:image(图片)、text(文本)、pdf(PDF文件)。使用 PDF 需要额外配置,根据文件中的 PDF_功能说明.md 进行配置"
"hint": "分析报告的输出格式:image(图片)、text(文本)、pdf(PDF文件)、html(HTML文件)。使用 PDF 需要额外配置,根据文件中的 PDF_功能说明.md 进行配置"
},
"report_template": {
"type": "string",
@@ -82,7 +82,7 @@
"type": "bool",
"description": "调试模式(建议关闭)",
"default": false,
"hint": "启用后,会在 plugin_data/debug_data 目录保存每次分析的平台 API 原始历史消息和 Prompt 原文,用于调试和优化提示词。"
"hint": "启用后,会在插件数据目录下的 debug_data 目录保存每次分析的平台 API 原始历史消息和 Prompt 原文,用于调试和优化提示词。"
},
"enable_base64_image": {
"type": "bool",
@@ -344,8 +344,8 @@
"pdf_output_dir": {
"type": "string",
"description": "PDF输出目录",
"default": "data/plugins/astrbot_plugin_qq_group_daily_analysis/reports",
"hint": "PDF报告文件的保存目录"
"default": "",
"hint": "PDF报告文件的保存目录。留空则自动使用插件数据目录下的 reports 目录。"
},
"browser_path": {
"type": "string",
@@ -361,6 +361,31 @@
}
}
},
"html": {
"description": "HTML 设置",
"type": "object",
"hint": "HTML 报告输出相关配置,包括外链 Base URL 和报告储存目录",
"items": {
"html_base_url": {
"type": "string",
"description": "外鏈 Base URL",
"default": "",
"hint": "用于生成外链的 Base URL(可留空)。如果设置了此项,发送报告时会在消息中提供超连結 (${BASE_URL}/${FILENAME})。留空则不提供外链。"
},
"html_output_dir": {
"type": "string",
"description": "报告储存目录",
"default": "",
"hint": "HTML报告文件的保存目录,用于存储生成的 HTML 文件和原始 JSON 数据。留空则自动使用插件数据目录下的 self_hosted_html_reports 目录。"
},
"html_filename_format": {
"type": "string",
"description": "HTML文件名格式",
"default": "群聊分析报告_{group_id}_{date}.html",
"hint": "HTML文件名格式,支持变量:{group_id}(群号)、{date}(日期)"
}
}
},
"qq_group_upload": {
"description": "群文件/群相册上传设置",
"type": "object",
+30 -4
View File
@@ -619,7 +619,7 @@ class GroupDailyAnalysis(Star):
pdf_path = await self.report_generator.generate_pdf_report(
analysis_result,
group_id,
avatar_url_getter=avatar_url_getter,
avatar_getter=avatar_url_getter,
nickname_getter=nickname_getter,
)
if pdf_path:
@@ -630,6 +630,31 @@ class GroupDailyAnalysis(Star):
else:
yield event.plain_result("⚠️ PDF 生成失败。")
elif output_format == "html":
html_path, json_path = await self.report_generator.generate_html_report(
analysis_result,
group_id,
avatar_url_getter=avatar_url_getter,
nickname_getter=nickname_getter,
)
if html_path:
# 发送 HTML 文件
if not await adapter.send_file(group_id, html_path):
yield event.chain_result(
[File(name=Path(html_path).name, file=html_path)]
)
# 如果配置了外链 Base URL,则也发送超链接
base_url = self.config_manager.get_html_base_url()
if base_url:
filename = Path(html_path).name
url = f"{base_url.rstrip('/')}/{filename}"
link_message = f"报告已生成: {url}"
if not await adapter.send_text(group_id, link_message):
yield event.plain_result(link_message)
else:
yield event.plain_result("⚠️ HTML 生成失败。")
else:
text_report = self.report_generator.generate_text_report(analysis_result)
if not await adapter.send_text(group_id, text_report):
@@ -640,7 +665,7 @@ class GroupDailyAnalysis(Star):
async def set_output_format(self, event: AstrMessageEvent, format_type: str = ""):
"""
设置分析报告输出格式(跨平台支持)
用法: /设置格式 [image|text|pdf]
用法: /设置格式 [image|text|pdf|html]
"""
group_id = self._get_group_id_from_event(event)
@@ -661,13 +686,14 @@ class GroupDailyAnalysis(Star):
• image - 图片格式 (默认)
• text - 文本格式
• pdf - PDF 格式 {pdf_status}
• html - HTML 格式
用法: /设置格式 [格式名称]""")
return
format_type = format_type.lower()
if format_type not in ["image", "text", "pdf"]:
yield event.plain_result("❌ 无效的格式类型,支持: image, text, pdf")
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:
@@ -279,16 +279,9 @@ class BaseAnalyzer(ABC, Generic[TDataObject]):
session_id: 会话ID
"""
try:
from pathlib import Path
from astrbot.api.star import StarTools
from astrbot.core.utils.astrbot_path import get_astrbot_plugin_data_path
plugin_name = "astrbot_plugin_qq_group_daily_analysis"
base_data_path = get_astrbot_plugin_data_path()
if isinstance(base_data_path, str):
base_data_path = Path(base_data_path)
data_path = base_data_path / plugin_name / "debug_data"
data_path = StarTools.get_data_dir() / "debug_data"
data_path.mkdir(parents=True, exist_ok=True)
file_name = f"{session_id}_{self.get_data_type()}.txt"
+2 -10
View File
@@ -461,18 +461,10 @@ class LLMAnalyzer(IAnalysisProvider):
"""
try:
import json
from pathlib import Path
from astrbot.core.utils.astrbot_path import (
get_astrbot_plugin_data_path,
)
from astrbot.api.star import StarTools
plugin_name = "astrbot_plugin_qq_group_daily_analysis"
base_data_path = get_astrbot_plugin_data_path()
if isinstance(base_data_path, str):
base_data_path = Path(base_data_path)
debug_dir = base_data_path / plugin_name / "debug_data"
debug_dir = StarTools.get_data_dir() / "debug_data"
debug_dir.mkdir(parents=True, exist_ok=True)
msg_file_path = debug_dir / f"{session_id}_messages.json"
+23 -5
View File
@@ -4,10 +4,9 @@
"""
import sys
from pathlib import Path
from astrbot.api import AstrBotConfig
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.api.star import StarTools
from ...utils.logger import logger
@@ -226,9 +225,7 @@ class ConfigManager:
def get_pdf_output_dir(self) -> str:
"""获取PDF输出目录"""
try:
plugin_name = "astrbot_plugin_qq_group_daily_analysis"
data_path = Path(get_astrbot_data_path())
default_path = data_path / "plugin_data" / plugin_name / "reports"
default_path = StarTools.get_data_dir() / "reports"
return self._get_group("pdf").get("pdf_output_dir", str(default_path))
except Exception:
return self._get_group("pdf").get(
@@ -250,6 +247,27 @@ class ConfigManager:
"pdf_filename_format", "群聊分析报告_{group_id}_{date}.pdf"
)
def get_html_output_dir(self) -> str:
"""获取HTML输出目录"""
try:
default_path = StarTools.get_data_dir() / "self_hosted_html_reports"
return self._get_group("html").get("html_output_dir", str(default_path))
except Exception:
return self._get_group("html").get(
"html_output_dir",
"data/plugins/astrbot_plugin_qq_group_daily_analysis/self_hosted_html_reports",
)
def get_html_base_url(self) -> str:
"""获取HTML外链Base URL"""
return self._get_group("html").get("html_base_url", "")
def get_html_filename_format(self) -> str:
"""获取HTML文件名格式"""
return self._get_group("html").get(
"html_filename_format", "群聊分析报告_{group_id}_{date}.html"
)
def get_topic_analysis_prompt(self, style: str = "topic_prompt") -> str:
"""获取话题分析提示词模板"""
prompts_config = self._get_group("prompts").get("topic_analysis_prompts", {})
+144 -7
View File
@@ -7,7 +7,9 @@ import asyncio
import base64
import os
import re
from datetime import datetime
from dataclasses import asdict, is_dataclass
from datetime import date, datetime
from enum import Enum
from pathlib import Path
import aiohttp
@@ -28,6 +30,7 @@ class ReportGenerator(IReportGenerator):
def __init__(self, config_manager, data_dir):
self._avatar_session = None
self.config_manager = config_manager
self.data_dir = data_dir
self.activity_visualizer = ActivityVisualizer()
self.html_templates = HTMLTemplates(config_manager) # 实例化HTML模板管理器
# 全局 T2I 渲染信号量,保护本地资源
@@ -36,7 +39,9 @@ class ReportGenerator(IReportGenerator):
self._render_semaphore = asyncio.Semaphore(max_concurrent)
# 运行时缓存,用于在一次分析任务中避免重复下载同一个头像
self._avatar_cache = Cache(str(data_dir / "avatar")) # user_id -> base64_uri
self._avatar_cache = Cache(
str(self.data_dir / "avatar")
) # user_id -> base64_uri
self._avatar_session_concurrent_semaphore = asyncio.Semaphore(
MAX_CONCURRENT_DOWNLOADS
)
@@ -205,13 +210,19 @@ class ReportGenerator(IReportGenerator):
self,
analysis_result: dict,
group_id: str,
avatar_url_getter=None,
avatar_getter=None,
nickname_getter=None,
) -> str | None:
"""生成PDF格式的分析报告"""
try:
# 确保输出目录存在(使用 asyncio.to_thread 避免阻塞)
output_dir = Path(self.config_manager.get_pdf_output_dir())
# 获取输出目录。如果未配置,则由 data_dir 推理得出。
output_dir = self.config_manager.get_pdf_output_dir()
if not output_dir:
output_dir = self.data_dir / "reports"
else:
output_dir = Path(output_dir)
# 确保输出目录存在 (使用 asyncio.to_thread 避免阻塞)
await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True)
# 生成文件名
@@ -225,7 +236,7 @@ class ReportGenerator(IReportGenerator):
render_data = await self._prepare_render_data(
analysis_result,
chart_template="activity_chart_pdf.html",
avatar_url_getter=avatar_url_getter,
avatar_url_getter=avatar_getter,
nickname_getter=nickname_getter,
)
logger.info(f"PDF 渲染数据准备完成,包含 {len(render_data)} 个字段")
@@ -254,6 +265,129 @@ class ReportGenerator(IReportGenerator):
logger.error(f"生成 PDF 报告失败: {e}")
return None
async def generate_html_report(
self,
analysis_result: dict,
group_id: str,
avatar_url_getter=None,
nickname_getter=None,
) -> tuple[str | None, str | None]:
"""
生成HTML格式的分析报告,保存到指定目录
Args:
analysis_result: 分析结果字典
group_id: 群组ID
avatar_url_getter: 异步回调函数,接收 user_id 返回 avatar_url/data
nickname_getter: 昵称获取函数
Returns:
tuple[str | None, str | None]: (html_path, json_path) - HTML文件路径和JSON文件路径
"""
try:
import json
# 获取输出目录。如果未配置,则由 data_dir 推理得出。
output_dir = self.config_manager.get_html_output_dir()
if not output_dir:
output_dir = self.data_dir / "self_hosted_html_reports"
else:
output_dir = Path(output_dir)
# 确保输出目录存在 (使用 asyncio.to_thread 避免阻塞)
await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True)
# 生成文件名
current_date = datetime.now().strftime("%Y%m%d")
current_time = datetime.now().strftime("%H%M%S")
html_filename = self.config_manager.get_html_filename_format().format(
group_id=group_id, date=current_date
)
# 为避免同一天多次分析覆盖,添加时间戳
html_filename_base = html_filename.rsplit(".", 1)[0]
html_filename = f"{html_filename_base}_{current_time}.html"
json_filename = f"{html_filename_base}_{current_time}.json"
html_path = output_dir / html_filename
json_path = output_dir / json_filename
# 准备渲染数据
render_data = await self._prepare_render_data(
analysis_result,
chart_template="activity_chart.html",
avatar_url_getter=avatar_url_getter,
nickname_getter=nickname_getter,
)
logger.info(f"HTML 渲染数据准备完成,包含 {len(render_data)} 个字段")
# 生成 HTML 内容(使用 Jinja2 渲染器,尝试 html_template.html,失败则回退到 image_template.html
html_content = None
try:
html_content = self.html_templates.render_template(
"html_template.html", **render_data
)
logger.info("使用 html_template.html 渲染成功")
except Exception as e:
logger.warning(
f"html_template.html 不存在或渲染失败,回退到 image_template.html: {e}"
)
html_content = self.html_templates.render_template(
"image_template.html", **render_data
)
logger.info("使用 image_template.html 渲染成功")
# 检查HTML内容是否有效
if not html_content:
logger.error("HTML报告渲染失败:返回空内容")
return None, None
logger.info(f"HTML 内容生成完成,长度: {len(html_content)} 字符")
# 保存 HTML 文件
await asyncio.to_thread(
html_path.write_text, html_content, encoding="utf-8"
)
logger.info(f"HTML 报告已保存: {html_path}")
def json_default_encoder(obj):
if hasattr(obj, "to_dict") and callable(obj.to_dict):
return obj.to_dict()
if is_dataclass(obj) and not isinstance(obj, type):
return asdict(obj)
if isinstance(obj, (datetime, date)):
return obj.isoformat()
if isinstance(obj, Enum):
return obj.value
if isinstance(obj, (set, tuple)):
return list(obj)
raise TypeError(
f"Object of type {type(obj).__name__} is not JSON serializable"
)
# 保存原始 JSON 数据
json_data = {
"analysis_result": analysis_result,
"group_id": group_id,
"generated_at": datetime.now().isoformat(),
}
await asyncio.to_thread(
json_path.write_text,
json.dumps(
json_data,
ensure_ascii=False,
indent=2,
default=json_default_encoder,
),
encoding="utf-8",
)
logger.info(f"JSON 数据已保存: {json_path}")
return str(html_path.absolute()), str(json_path.absolute())
except Exception as e:
logger.error(f"生成 HTML 报告失败: {e}", exc_info=True)
return None, None
def generate_text_report(self, analysis_result: dict) -> str:
"""生成文本格式的分析报告"""
stats = analysis_result["statistics"]
@@ -555,7 +689,10 @@ class ReportGenerator(IReportGenerator):
"""
# 1. 检查缓存 (仅包含成功的头像数据)
if avatar_id in self._avatar_cache:
return self._avatar_cache[avatar_id]
data = self._avatar_cache[avatar_id]
if isinstance(data, str):
return data
return str(data)
# 2. 尝试获取头像字节流
avatar_bytes = await self._get_user_avatar_bytes(avatar_id, avatar_url_getter)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,541 @@
<!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>
: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: 20px;
-webkit-font-smoothing: antialiased;
}
.container {
width: 100%;
max-width: none;
margin: 0 auto;
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
/* 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.85em;
border-top: 1px solid var(--border);
line-height: 1.8;
}
/* 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>
{% if chat_quality_html %}
<div class="section full-width-section">
<h2 class="section-title">群聊质量分析</h2>
{{ chat_quality_html | safe }}
</div>
{% endif %}
{{topics_html | safe}}
{{titles_html | safe}}
{{quotes_html | safe}}
</div>
<div class="footer">
由 AstrBot 群日常分析插件 生成 | {{current_datetime}}<br>
SXP-Simon/astrbot_plugin_qq_group_daily_analysis<br>
<span style="opacity: 0.7;">AI分析消耗:{{total_tokens}} tokens (输入: {{prompt_tokens}}, 输出: {{completion_tokens}})</span>
</div>
</div>
</body>
</html>
@@ -0,0 +1,621 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Night Mode Analysis Portal</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>
:root {
--bg-deep: #0a0a0a;
--bg-panel: #141414;
--bg-card: #1c1c1c;
--text-primary: #ffffff;
--text-secondary: #cccccc;
--accent-orange: #ff9900;
/* Primary */
--accent-blue: #3399ff;
/* Secondary */
--accent-red: #ff3366;
--accent-green: #ff9900;
/* Changed to Orange per user request */
--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;
}
body {
margin: 0;
background-color: var(--bg-deep);
color: var(--text-primary);
font-family: var(--font-body);
background-image:
radial-gradient(circle at 50% 10%, rgba(255, 153, 0, 0.08) 0%, transparent 70%),
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 100% 100%, 30px 30px, 30px 30px;
min-height: 100vh;
font-size: 18px;
position: relative;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.05) 50%);
background-size: 100% 4px;
pointer-events: none;
z-index: 9999;
opacity: 0.2;
}
.layout {
max-width: 1200px;
margin: 0 auto;
padding: 60px 40px;
}
/* Terminal Window Style */
.window {
background: rgba(20, 20, 20, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.6);
margin-bottom: 40px;
}
.window-header {
background: #1e1e1e;
padding: 12px 20px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.traffic-lights {
display: flex;
gap: 8px;
}
.light {
width: 12px;
height: 12px;
border-radius: 50%;
}
.light.red {
background: #ff5f56;
}
.light.yellow {
background: #ffbd2e;
}
.light.green {
background: #ff9900;
}
.window-title {
margin-left: 20px;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-secondary);
flex: 1;
}
/* Top Navigation Bar from Reference */
.top-nav {
display: flex;
gap: 20px;
margin-bottom: 40px;
font-family: var(--font-mono);
font-size: 0.85rem;
align-items: center;
}
.nav-item {
color: var(--text-secondary);
padding: 6px 12px;
border-radius: 4px;
border: 1px solid transparent;
display: flex;
align-items: center;
gap: 8px;
}
.nav-item.active {
color: var(--accent-orange);
background: rgba(255, 153, 0, 0.05);
border-color: rgba(255, 153, 0, 0.2);
}
/* Typography */
.prompt {
color: var(--accent-orange);
/* Changed from green */
font-weight: bold;
}
.keyword {
color: var(--accent-red);
}
.comment {
color: var(--text-secondary);
}
h1 {
font-size: 5rem;
font-weight: 900;
margin: 0 0 25px;
letter-spacing: -3px;
line-height: 0.9;
background: linear-gradient(135deg, #fff 60%, var(--accent-orange));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.cursor {
display: inline-block;
width: 15px;
height: 1.2em;
background: var(--text-primary);
vertical-align: middle;
margin-left: 5px;
animation: blink 1s infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: rgba(28, 28, 28, 0.6);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 153, 0, 0.1);
padding: 24px;
border-radius: 12px;
transition: all 0.3s ease;
}
.stat-card:hover {
border-color: var(--accent-orange);
background: rgba(28, 28, 28, 0.8);
transform: translateY(-5px);
}
.stat-label {
font-family: var(--font-mono);
color: var(--text-secondary);
font-size: 0.85rem;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.stat-label::before {
content: '>';
color: var(--accent-orange);
}
.stat-value {
font-size: 3.5rem;
font-weight: 800;
color: var(--accent-orange);
font-family: var(--font-mono);
text-shadow: 0 0 20px rgba(255, 153, 0, 0.3);
}
/* Main Layout Grid */
.main-container {
display: grid;
grid-template-columns: 58% 42%;
/* Golden ratio-ish split */
gap: 40px;
align-items: flex-start;
}
.left-col {
display: flex;
flex-direction: column;
gap: 40px;
}
.right-col {
display: flex;
flex-direction: column;
gap: 40px;
}
.section-header {
font-family: var(--font-mono);
font-size: 1.1rem;
margin-bottom: 25px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 10px;
}
/* Code Block Container */
.code-block {
background: rgba(20, 20, 20, 0.8);
border: 1px solid rgba(255, 153, 0, 0.15);
padding: 30px;
border-radius: 12px;
font-family: var(--font-mono);
line-height: 1.6;
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.2);
}
/* Chart Styling - Reference inspired */
.chart-container {
height: 300px;
position: relative;
padding: 40px 10px 50px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 2px;
overflow: visible;
}
.chart-svg {
width: 100%;
height: 100%;
}
.chart-line {
fill: none;
stroke: var(--accent-orange);
stroke-width: 3;
}
.chart-area {
fill: url(#areaGradient);
opacity: 0.3;
}
/* Horizontal Activity Chart Elements */
.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: 12px;
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-bar-v::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(255, 255, 255, 0.05), transparent);
}
.activity-tick {
font-family: var(--font-mono);
font-size: 0.5rem;
color: var(--text-secondary);
margin-top: 10px;
transform: rotate(-60deg);
transform-origin: top center;
white-space: nowrap;
}
.activity-val-top {
font-family: var(--font-mono);
font-size: 0.55rem;
color: var(--text-primary);
margin-bottom: 4px;
opacity: 0.6;
}
/* Vertical Flow Elements */
.user-grid,
.quote-container,
.topic-list {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
@media (max-width: 900px) {
.user-grid,
.quote-container {
grid-template-columns: 1fr;
}
}
.user-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
padding: 20px;
border-radius: 8px;
display: flex;
gap: 20px;
transition: border-color 0.2s;
}
.user-card:hover {
border-color: var(--accent-blue);
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 8px;
border: 1px solid #333;
}
.user-info h4 {
margin: 0 0 5px;
font-size: 1.1rem;
}
.tag {
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid;
margin-right: 8px;
}
.tag.mbti {
color: #ffcc00;
border-color: rgba(255, 204, 0, 0.3);
background: rgba(255, 204, 0, 0.05);
}
.tag.title {
color: var(--accent-blue);
border-color: rgba(51, 153, 255, 0.3);
background: rgba(51, 153, 255, 0.05);
}
.topic-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.topic-item {
padding: 20px;
border-left: 3px solid var(--accent-orange);
background: rgba(255, 255, 255, 0.02);
}
.topic-title {
font-weight: bold;
font-size: 1.5rem;
margin-bottom: 12px;
color: var(--accent-orange);
}
/* Quotes */
.quote-item {
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
display: flex;
gap: 20px;
align-items: flex-start;
background: rgba(30, 30, 30, 0.4);
backdrop-filter: blur(5px);
margin-bottom: 20px;
}
.quote-avatar {
width: 45px;
height: 45px;
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: 1.1rem;
color: var(--accent-orange);
margin-bottom: 12px;
line-height: 1.5;
}
.quote-author {
font-family: var(--font-mono);
font-size: 0.9rem;
color: var(--accent-blue);
margin-bottom: 8px;
}
/* Footer */
footer {
margin-top: 80px;
padding-top: 40px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.9rem;
color: var(--text-secondary);
}
.footer-token {
color: var(--accent-green);
}
</style>
</head>
<body>
<div class="layout">
<!-- Top Navigation Mockup -->
<div class="top-nav">
<div class="nav-item active">● ready</div>
<div class="nav-item">~/analysis</div>
<div style="flex: 1"></div>
<div class="nav-item">$ ai --summary</div>
<div class="nav-item">$ cd /groups</div>
<div class="nav-item">$ api --status</div>
<div class="nav-item">🌐 EN</div>
<div class="user-avatar" style="width: 25px; height: 25px; border-radius: 50%;"></div>
</div>
<div class="main-container">
<!-- Left Side: Analytical Data -->
<div class="left-col">
<div>
<div class="comment">// main.ts</div>
<h1><span class="prompt">&gt;</span> Group Activity<br>Insights<span class="cursor"></span></h1>
<div class="comment" style="margin-bottom: 30px;">// Report for {{current_date}}</div>
<div class="code-block">
<span class="keyword">const</span> stats = {<br>
&nbsp;&nbsp;messages: <span style="color: var(--accent-orange);">{{message_count}}</span>,<br>
&nbsp;&nbsp;active_users: <span
style="color: var(--accent-orange);">{{participant_count}}</span>,<br>
&nbsp;&nbsp;peak_time: <span
style="color: var(--accent-blue);">'{{most_active_period}}'</span><br>
};<br>
<span style="color: var(--accent-green);">// Total characters: {{total_characters}}</span>
</div>
</div>
{% if chat_quality_html %}
<div>
<div class="section-header">
<span class="prompt">$</span> analyze --quality
</div>
{{ chat_quality_html | safe }}
</div>
{% endif %}
<!-- Topics moved into left column -->
{% if topics_html %}
<div>
<div class="section-header">
<span class="prompt">$</span> ls ./topics
</div>
<div class="topic-list">
{{topics_html | safe}}
</div>
</div>
{% endif %}
</div>
<!-- Right Side: Visuals & Members -->
<div class="right-col">
<div class="window">
<div class="window-header">
<div class="traffic-lights">
<div class="light red"></div>
<div class="light yellow"></div>
<div class="light green"></div>
</div>
<div class="window-title">trend-analytics.tsx</div>
</div>
<div class="chart-container">
{{hourly_chart_html | safe}}
</div>
</div>
<!-- User Titles grouped here -->
{% if titles_html %}
<div style="margin-top: 20px;">
<div class="section-header">
<span class="prompt">$</span> cat ./user_titles
</div>
<div class="user-grid">
{{titles_html | safe}}
</div>
</div>
{% endif %}
<!-- Golden Quotes grouped here -->
{% if quotes_html %}
<div style="margin-top: 20px;">
<div class="section-header">
<span class="prompt">$</span> grep -r "golden_quotes"
</div>
<div class="quote-container">
{{quotes_html | safe}}
</div>
</div>
{% endif %}
</div>
</div>
<footer>
<div>SYSTEM_REPORT // {{current_datetime}}</div>
<div>
TOKENS: <span class="footer-token">{{total_tokens}}</span>
[P: {{prompt_tokens}} | C: {{completion_tokens}}]
</div>
<div style="color: var(--accent-orange);">SXP-Simon/astrbot_plugin_qq_group_daily_analysis 群分析</div>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,843 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Retro Futurism Daily Export</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;
}
@font-face {
font-family: "JetBrains-Fallback";
src: local("Consolas"), local("Monaco"), local("Courier New");
font-weight: 400 700;
}
@font-face {
font-family: "NotoSans-Fallback";
src: local("Microsoft YaHei"), local("PingFang SC"), local("Hiragino Sans GB"), local("SimHei");
font-weight: 400 700;
}
:root {
/* Better Palette - More "Industrial Retro" */
--c-bg: #F8F4E8;
/* Slightly cleaner vintage paper */
--c-text: #1A1A1A;
--c-accent: #D35400;
/* Richer orange */
--c-accent-glow: rgba(211, 84, 0, 0.4);
--c-dec-1: #2E4053;
/* Navy slate */
--c-dec-2: #A93226;
/* Deep rust */
--c-dec-3: #D4AC0D;
/* Industrial gold */
--c-grid: rgba(26, 26, 26, 0.05);
--c-scanline: rgba(18, 16, 16, 0.02);
/* Fonts */
--font-display: "Oswald", "Oswald-Fallback", Impact, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--font-body: "Noto Sans SC", sans-serif;
}
* {
box-sizing: border-box;
}
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),
/* Noise Texture */
url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.05'/%3E%3C/svg%3E");
background-size: 40px 40px, 100% 100%;
min-height: 100vh;
overflow-x: hidden;
letter-spacing: -0.01em;
font-size: 18px;
/* Increased base font size */
}
/* Layout Container with Stacked Paper Effect */
.layout {
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 100px 80px;
/* Increased padding */
position: relative;
background: var(--c-bg);
border-right: 4px solid var(--c-text);
border-bottom: 4px solid var(--c-text);
box-shadow: 20px 20px 0 rgba(0, 0, 0, 0.05);
/* Increased shadow */
}
.layout::before {
content: '';
position: absolute;
top: 15px;
left: -15px;
width: 100%;
height: 100%;
border: 1px solid var(--c-text);
z-index: -1;
opacity: 0.3;
}
/* Decorative Color Bar */
.color-bar {
display: flex;
gap: 4px;
margin-bottom: 40px;
border: 3px solid var(--c-text);
padding: 4px;
display: inline-flex;
}
.c-block {
width: 100px;
height: 12px;
}
.cb-1 {
background: var(--c-dec-1);
}
.cb-2 {
background: var(--c-dec-2);
}
.cb-3 {
background: var(--c-dec-3);
}
/* Header */
header {
margin-bottom: 100px;
padding-bottom: 60px;
border-bottom: 2px double var(--c-text);
}
.header-meta {
font-family: var(--font-mono);
font-size: 1rem;
/* Increased */
color: var(--c-dec-1);
margin-bottom: 20px;
letter-spacing: 0.4rem;
text-transform: uppercase;
font-weight: bold;
}
h1 {
font-family: var(--font-display);
font-size: 7rem;
/* Significantly Increased (SCRAPBOOK style) */
line-height: 0.85;
margin: 0 0 40px;
text-transform: uppercase;
letter-spacing: -0.2rem;
color: var(--c-text);
filter: drop-shadow(6px 6px 0 var(--c-accent-glow));
}
h1 .highlight {
color: var(--c-accent);
position: relative;
}
h1 .highlight::after {
content: '';
position: absolute;
left: 0;
bottom: 10px;
width: 100%;
height: 20px;
background: var(--c-dec-3);
z-index: -1;
opacity: 0.5;
}
.header-icons {
display: flex;
gap: 20px;
margin-top: 40px;
}
.header-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--c-text);
background: #fff;
box-shadow: 4px 4px 0 var(--c-text);
}
.header-icon svg {
width: 32px;
height: 32px;
fill: var(--c-text);
}
.header-brief {
max-width: 800px;
color: rgba(0, 0, 0, 0.7);
line-height: 1.8;
margin: 40px 0;
font-size: 1.4rem;
/* Increased */
font-family: var(--font-mono);
border-left: 8px solid var(--c-accent);
padding-left: 30px;
}
.header-meta-row {
display: flex;
gap: 80px;
margin-top: 60px;
}
.meta-block {
display: flex;
flex-direction: column;
}
.meta-label {
font-family: var(--font-mono);
font-size: 0.9rem;
letter-spacing: 0.2rem;
text-transform: uppercase;
color: var(--c-dec-2);
margin-bottom: 12px;
}
.meta-value {
font-family: var(--font-mono);
font-size: 1.4rem;
font-weight: bold;
}
/* CRT Console */
.crt-console {
background: #111;
color: #00FF41;
padding: 40px;
font-family: var(--font-mono);
border: 5px solid var(--c-text);
border-radius: 6px;
box-shadow: inset 0 0 60px rgba(0, 0, 0, 1), 10px 10px 0 rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
grid-column: span 12;
}
.crt-console::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%),
linear-gradient(90deg, rgba(255, 0, 0, 0.05), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.05));
background-size: 100% 6px, 4px 100%;
pointer-events: none;
}
.crt-header {
display: flex;
gap: 15px;
margin-bottom: 30px;
opacity: 0.7;
}
.crt-btn {
width: 12px;
height: 12px;
background: #333;
border: 1px solid #555;
}
.crt-title {
color: #00FF41;
text-shadow: 0 0 8px #00FF41;
font-size: 1rem;
letter-spacing: 0.4rem;
margin-bottom: 15px;
}
.crt-value {
font-family: var(--font-display);
font-size: 6rem;
/* Increased */
color: #fff;
text-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
margin: 15px 0;
letter-spacing: -3px;
}
.crt-sub {
opacity: 0.8;
font-size: 1rem;
border-top: 2px solid #333;
padding-top: 15px;
display: flex;
justify-content: space-between;
}
/* Grid System */
.grid-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 40px;
margin-bottom: 100px;
}
/* Section Labels */
.section-label {
grid-column: span 12;
font-family: var(--font-display);
font-size: 3.5rem;
/* Increased */
text-transform: uppercase;
border-bottom: 6px solid var(--c-text);
padding-bottom: 20px;
margin-bottom: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-label span {
background: var(--c-text);
color: var(--c-bg);
padding: 8px 20px;
font-size: 1.1rem;
font-family: var(--font-mono);
letter-spacing: 0.4rem;
}
/* Stat Box */
.stat-box {
grid-column: span 3;
border: 3px solid var(--c-text);
padding: 40px 30px;
background: #fff;
position: relative;
box-shadow: 8px 8px 0 var(--c-text);
}
.stat-label {
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: bold;
text-transform: uppercase;
color: var(--c-dec-1);
margin-bottom: 20px;
letter-spacing: 0.3rem;
display: block;
}
.stat-value {
font-family: var(--font-display);
font-size: 5rem;
color: var(--c-accent);
line-height: 0.8;
margin-bottom: 15px;
}
.stat-note {
font-size: 1rem;
color: #666;
font-family: var(--font-mono);
text-transform: uppercase;
}
/* Content Card */
.content-card {
grid-column: span 12;
border: 3px solid var(--c-text);
background: #fff;
padding: 60px;
box-shadow: 12px 12px 0 var(--c-grid);
}
/* Activity Chart Redesign: Horizontal side-by-side bars */
.activity-container {
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 350px;
/* Chart height */
padding: 20px 0;
border-bottom: 3px solid var(--c-text);
margin-bottom: 40px;
}
.activity-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
gap: 15px;
}
.activity-bar-vertical {
width: 70%;
background: var(--c-accent);
border: 2px solid var(--c-text);
height: var(--h, 0%);
/* Height from style */
position: relative;
min-height: 4px;
box-shadow: 4px 0 0 rgba(0, 0, 0, 0.05);
}
.activity-bar-vertical::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), transparent);
}
.activity-label-hour {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: bold;
transform: rotate(-45deg);
white-space: nowrap;
margin-top: 10px;
color: var(--c-dec-1);
}
.activity-col-count {
font-family: var(--font-mono);
font-size: 0.8rem;
font-weight: bold;
color: var(--c-text);
margin-bottom: 5px;
}
/* Topic Items */
.topic-list {
display: grid;
grid-template-columns: 1fr;
gap: 40px;
margin-bottom: 100px;
}
.topic-item {
display: flex;
border: 3px solid var(--c-text);
background: #fff;
position: relative;
box-shadow: 12px 12px 0 var(--c-text);
padding: 40px;
}
.topic-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 16px;
background: repeating-linear-gradient(45deg, var(--c-dec-2), var(--c-dec-2) 15px, transparent 15px, transparent 30px);
}
.topic-content {
margin-left: 30px;
flex: 1;
}
.topic-title {
font-family: var(--font-display);
font-size: 3rem;
margin: 0 0 20px;
text-transform: uppercase;
}
.topic-meta {
font-family: var(--font-mono);
font-size: 1rem;
color: var(--c-dec-1);
margin-bottom: 20px;
background: var(--c-grid);
padding: 8px 15px;
display: inline-block;
}
.topic-detail {
font-size: 1.4rem;
line-height: 1.8;
color: #333;
}
/* User Titles */
.title-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 50px;
}
.title-item {
display: flex;
gap: 30px;
align-items: center;
/* Center aligned */
padding: 40px;
border: 2px solid var(--c-text);
background: #fff;
box-shadow: 8px 8px 0 var(--c-grid);
}
.title-avatar-wrap {
width: 120px;
height: 120px;
flex-shrink: 0;
/* Important! */
border: 3px solid var(--c-text);
box-shadow: 6px 6px 0 var(--c-dec-3);
overflow: hidden;
background: #eee;
}
.title-avatar-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(100%) contrast(1.1);
}
.title-info {
flex: 1;
}
.title-info h4 {
margin: 0 0 10px;
font-family: var(--font-display);
font-size: 2.2rem;
line-height: 1;
}
.title-badge {
font-family: var(--font-mono);
font-size: 0.9rem;
background: var(--c-text);
color: #fff;
padding: 5px 12px;
margin-bottom: 15px;
display: inline-flex;
gap: 10px;
}
.title-reason {
font-size: 1.2rem;
line-height: 1.6;
margin-top: 15px;
color: #444;
}
/* Quotes */
.quote-list {
display: grid;
gap: 30px;
}
.quote-item {
display: flex;
gap: 30px;
align-items: flex-start;
padding: 40px;
background: #fff;
border: 3px solid var(--c-text);
border-left: 20px solid var(--c-accent);
box-shadow: 12px 12px 0 rgba(0, 0, 0, 0.05);
}
.quote-avatar {
width: 90px;
height: 90px;
flex-shrink: 0;
border: 2px 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: "JetBrains Mono", monospace;
font-size: 1.6rem;
line-height: 1.6;
margin-bottom: 20px;
font-style: italic;
position: relative;
}
.quote-content::before {
content: '"';
position: absolute;
left: -20px;
top: -10px;
font-size: 3rem;
opacity: 0.2;
color: var(--c-accent);
}
.quote-author {
font-weight: bold;
font-family: var(--font-display);
font-size: 1.6rem;
text-transform: uppercase;
color: var(--c-dec-1);
}
.quote-reason-small {
font-size: 1rem;
color: #666;
margin-top: 10px;
font-family: var(--font-mono);
}
/* Footer */
footer {
margin-top: 100px;
padding: 60px 0;
border-top: 8px solid var(--c-text);
font-family: var(--font-mono);
font-size: 0.9rem;
display: flex;
flex-direction: column;
gap: 20px;
}
.footer-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-tokens {
display: flex;
gap: 30px;
opacity: 0.6;
}
.footer-author {
color: var(--c-accent);
font-weight: bold;
}
.crt-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.05) 50%);
background-size: 100% 4px;
pointer-events: none;
z-index: 1000;
}
</style>
</head>
<body>
<div class="crt-overlay"></div>
<div class="layout">
<header>
<div class="color-bar">
<div class="c-block cb-1"></div>
<div class="c-block cb-2"></div>
<div class="c-block cb-3"></div>
</div>
<div class="header-meta">System Report // {{current_date}}</div>
<h1>Daily<br><span class="highlight">Analysis</span><br>Export</h1>
<div class="header-icons">
<div class="header-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
<div class="header-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z" />
</svg>
</div>
<div class="header-icon">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z" />
</svg>
</div>
</div>
<p class="header-brief">
Daily activity analysis report, generated based on message statistics and AI analysis
</p>
<div class="header-meta-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">Peak Window</div>
<span class="meta-value">{{most_active_period}}</span>
</div>
</div>
</header>
<!-- Stats Grid -->
<div class="grid-container">
<div class="stat-box">
<span class="stat-label">Messages</span>
<div class="stat-value">{{message_count}}</div>
<div class="stat-note">累计消息数量</div>
</div>
<div class="stat-box">
<span class="stat-label">Participants</span>
<div class="stat-value">{{participant_count}}</div>
<div class="stat-note">参与者计数</div>
</div>
<div class="stat-box">
<span class="stat-label">Characters</span>
<div class="stat-value">{{total_characters}}</div>
<div class="stat-note">文本总字数</div>
</div>
<div class="stat-box">
<span class="stat-label">Emoji</span>
<div class="stat-value">{{emoji_count}}</div>
<div class="stat-note">表情符号统计</div>
</div>
</div>
<!-- CRT Console -->
<div class="grid-container">
<div class="crt-console">
<div class="crt-header">
<span class="crt-btn crt-btn-1"></span>
<span class="crt-btn crt-btn-2"></span>
<span class="crt-btn crt-btn-3"></span>
</div>
<div class="crt-label">Peak Activity Window</div>
<div class="crt-value">{{most_active_period}}</div>
<div class="crt-sub">活跃序列 SCAN // 消息流强度 {{message_count}}</div>
</div>
</div>
<!-- Hourly Activity -->
<div class="section-label">
Hourly Broadcast <span>ACTIVITY_SPECTRUM</span>
</div>
<div class="grid-container">
<div class="content-card">
<div class="activity-wrapper">
{{hourly_chart_html|safe}}
</div>
</div>
</div>
<!-- Chat Quality Review -->
{% if chat_quality_html %}
<div class="section-label">
Quality Analysis <span>STABILITY_METRICS</span>
</div>
<div class="grid-container">
<div style="grid-column: span 12;">
{{ chat_quality_html|safe }}
</div>
</div>
{% endif %}
<!-- Topics -->
{% if topics_html %}
<div class="section-label">
Thread Matrix <span>TOPICS_MODULE</span>
</div>
<div class="topic-list">
{{topics_html|safe}}
</div>
{% endif %}
<!-- Titles -->
{% if titles_html %}
<div class="section-label">
Operator Registry <span>TITLES_MODULE</span>
</div>
<div class="grid-container">
<div class="content-card">
<div class="title-grid">
{{titles_html|safe}}
</div>
</div>
</div>
{% endif %}
<!-- Quotes -->
{% if quotes_html %}
<div class="section-label">
Golden Lines <span>QUOTES_MODULE</span>
</div>
<div class="quote-list">
{{quotes_html|safe}}
</div>
{% endif %}
<footer>
<div class="footer-row">
<div>CRT_EXPORT_CHANNEL // AstrBot 群日常分析插件</div>
<div class="footer-author">SXP-Simon / astrbot_plugin_qq_group_daily_analysis</div>
</div>
<div class="footer-row">
<div class="footer-tokens">
<span>Total Tokens // {{total_tokens}}</span>
<span>Prompt // {{prompt_tokens}}</span>
<span>Completion // {{completion_tokens}}</span>
</div>
</div>
<div>{{current_datetime}} // END_OF_LINE_</div>
</footer>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Simple Report</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
}
.section {
margin-bottom: 20px;
border: 1px solid #ccc;
padding: 10px;
}
h1,
h2 {
color: #333;
}
</style>
</head>
<body>
<h1>群聊日报 - {{current_date}}</h1>
<div class="section">
<h2>基础统计</h2>
<p>消息数: {{message_count}}</p>
<p>参与人数: {{participant_count}}</p>
<p>总字符数: {{total_characters}}</p>
<p>表情数: {{emoji_count}}</p>
<p>最活跃时段: {{most_active_period}}</p>
</div>
<div class="section">
<h2>活跃度图表</h2>
{{hourly_chart_html | safe}}
</div>
{% if chat_quality_html %}
<div class="section">
<h2>群聊质量分析</h2>
{{ chat_quality_html | safe }}
</div>
{% endif %}
{{topics_html | safe}}
{{titles_html | safe}}
{{quotes_html | safe}}
<div class="footer" style="margin-top: 30px; padding: 15px; border-top: 1px solid #ccc; color: #666; font-size: 0.85em; text-align: center; line-height: 1.8;">
Generated by AstrBot | {{current_datetime}}<br>
SXP-Simon/astrbot_plugin_qq_group_daily_analysis<br>
<span style="opacity: 0.7;">Token Usage: {{total_tokens}} (Prompt: {{prompt_tokens}}, Completion: {{completion_tokens}})</span>
</div>
</body>
</html>
@@ -0,0 +1,690 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>春节特供 · 群聊日报</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>
:root {
--red-festive: #b71c1c;
--red-light: #d32f2f;
--gold-primary: #ffca28;
--gold-dark: #f9a825;
--wood-dark: #3e2723;
--bg-paper: #fff9e6;
/* 宣纸色 */
--text-main: #3e2723;
--text-gold: #ffca28;
--font-calligraphy: 'Ma Shan Zheng', cursive;
--font-heading: 'ZCOOL XiaoWei', serif;
--font-body: 'Noto Serif SC', serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: var(--red-festive);
color: var(--text-main);
font-family: var(--font-body);
background-image:
radial-gradient(var(--red-light) 1px, transparent 1px),
linear-gradient(45deg, rgba(255, 202, 40, 0.05) 25%, transparent 25%, transparent 50%, rgba(255, 202, 40, 0.05) 50%, rgba(255, 202, 40, 0.05) 75%, transparent 75%, transparent);
background-size: 40px 40px, 100px 100px;
min-height: 100vh;
padding: 40px 20px;
display: flex;
justify-content: center;
}
.container {
max-width: 1100px;
width: 100%;
background: var(--bg-paper);
border: 15px solid var(--red-festive);
outline: 3px solid var(--gold-primary);
outline-offset: -10px;
position: relative;
padding: 60px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
border-radius: 4px;
}
/* 装饰角标 */
.corner {
position: absolute;
width: 80px;
height: 80px;
border: 5px solid var(--gold-primary);
z-index: 10;
}
.corner-tl {
top: -10px;
left: -10px;
border-right: none;
border-bottom: none;
border-radius: 15px 0 0 0;
}
.corner-tr {
top: -10px;
right: -10px;
border-left: none;
border-bottom: none;
border-radius: 0 15px 0 0;
}
.corner-bl {
bottom: -10px;
left: -10px;
border-right: none;
border-top: none;
border-radius: 0 0 0 15px;
}
.corner-br {
bottom: -10px;
right: -10px;
border-left: none;
border-top: none;
border-radius: 0 0 15px 0;
}
/* Header */
.sf-header {
text-align: center;
margin-bottom: 60px;
position: relative;
}
.sf-title-wrap {
display: inline-block;
padding: 20px 60px;
background: var(--red-festive);
border: 3px double var(--gold-primary);
color: var(--gold-primary);
transform: skew(-5deg);
}
.sf-header h1 {
font-family: var(--font-calligraphy);
font-size: 5rem;
margin: 0;
line-height: 1.2;
text-shadow: 3px 3px 0px rgba(0, 0, 0, 0.2);
}
.sf-date {
font-family: var(--font-heading);
font-size: 1.5rem;
margin-top: 15px;
color: var(--red-festive);
font-weight: bold;
}
/* Layout Overhaul like Scrapbook */
.sf-stats-wrapper {
display: flex;
gap: 25px;
margin-bottom: 40px;
align-items: stretch;
}
.sf-stats-grid {
flex: 2;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.sf-hongbao {
background: var(--red-festive);
padding: 20px 15px;
border-radius: 12px 12px 60px 60px;
color: var(--gold-primary);
text-align: center;
border: 2px solid var(--gold-primary);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
.sf-hongbao::before {
content: '福';
position: absolute;
top: -10px;
right: -10px;
font-family: var(--font-calligraphy);
font-size: 4rem;
opacity: 0.1;
transform: rotate(15deg);
}
.sf-peak-badge {
flex: 1.2;
background: var(--gold-primary);
border: 4px solid var(--red-festive);
border-radius: 20px;
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
position: relative;
box-shadow: 8px 8px 0 var(--red-festive);
}
.sf-peak-badge::after {
content: 'YEAR OF THE HORSE';
/* Based on user current date 2026 */
position: absolute;
bottom: 10px;
font-size: 0.7rem;
letter-spacing: 2px;
color: var(--red-festive);
font-weight: bold;
opacity: 0.5;
}
.sf-main-flow {
display: flex;
flex-direction: column;
gap: 50px;
}
/* Window Frame for Chart */
.sf-window {
background: #fff;
border: 10px solid var(--wood-dark);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.05);
padding: 20px;
position: relative;
min-height: 320px;
display: flex;
flex-direction: column;
overflow-x: auto;
}
.sf-window-title {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background: var(--wood-dark);
color: var(--gold-primary);
padding: 5px 25px;
font-size: 0.9rem;
font-family: var(--font-mono);
border-radius: 4px;
}
/* Chart Elements */
.sf-chart-wrapper {
width: 100%;
background: #fff;
}
.sf-chart-container {
height: 250px;
display: flex;
align-items: flex-end;
justify-content: space-around;
gap: 2px;
padding: 10px 5px 30px;
min-width: 400px;
}
.sf-chart-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
}
.sf-chart-bar {
width: 80%;
background: linear-gradient(to top, var(--red-festive), var(--gold-primary));
height: var(--h, 0%);
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);
transition: height 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.sf-chart-tick {
font-size: 0.6rem;
transform: rotate(-45deg);
margin-top: 5px;
white-space: nowrap;
}
.sf-chart-val {
font-size: 0.6rem;
font-weight: bold;
color: var(--red-festive);
margin-bottom: 2px;
}
/* Topic Scrolls */
.sf-section-box {
margin-bottom: 40px;
}
.sf-section-title {
font-family: var(--font-calligraphy);
font-size: 2.4rem;
color: var(--red-festive);
margin-bottom: 35px;
display: flex;
align-items: center;
gap: 15px;
position: relative;
padding-bottom: 12px;
border-bottom: 2px solid var(--red-festive);
background: linear-gradient(to right, rgba(183, 28, 28, 0.05), transparent);
}
.sf-section-title::before {
content: '☁️';
/* Cloud decoration */
font-size: 1.2rem;
opacity: 0.6;
}
.sf-section-title::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 80px;
height: 8px;
background: var(--gold-primary);
clip-path: polygon(0 0, 100% 0, 85% 100%, 0 100%);
}
.sf-topic-card {
background: #fff;
padding: 25px;
margin-bottom: 25px;
border: 2px solid var(--red-festive);
box-shadow: 8px 8px 0 rgba(183, 28, 28, 0.1);
position: relative;
}
.sf-topic-title {
font-family: var(--font-heading);
font-size: 1.8rem;
color: var(--red-festive);
margin-bottom: 10px;
}
.sf-topic-meta {
font-size: 0.9rem;
color: #666;
margin-bottom: 15px;
font-style: italic;
}
.sf-topic-detail {
line-height: 1.8;
font-size: 1.1rem;
}
/* User Grid */
.sf-user-grid {
display: flex;
flex-direction: column;
gap: 20px;
}
.sf-user-card {
display: flex;
flex-direction: column;
gap: 15px;
padding: 25px;
background: #fff;
border-left: 4px solid var(--red-festive);
border-right: 4px solid var(--red-festive);
border-top: 1px solid var(--red-festive);
border-bottom: 1px solid var(--red-festive);
position: relative;
box-shadow: 6px 6px 20px rgba(183, 28, 28, 0.1);
background-image: radial-gradient(rgba(183, 28, 28, 0.02) 1px, transparent 1px);
background-size: 15px 15px;
}
.sf-user-card::before {
content: '春';
position: absolute;
bottom: 5px;
right: 5px;
font-family: var(--font-calligraphy);
font-size: 3rem;
color: var(--red-festive);
opacity: 0.05;
pointer-events: none;
}
.sf-card-header {
display: flex;
align-items: center;
gap: 18px;
border-bottom: 1px solid rgba(183, 28, 28, 0.1);
padding-bottom: 15px;
}
.sf-avatar-wrap {
width: 65px;
height: 65px;
flex-shrink: 0;
border: 2px solid var(--gold-primary);
background: var(--red-festive);
padding: 3px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
position: relative;
}
.sf-avatar {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.sf-avatar-placeholder {
font-family: var(--font-calligraphy);
font-size: 3rem;
color: var(--red-festive);
}
.sf-user-name {
margin: 0 0 5px;
font-family: var(--font-heading);
font-size: 1.4rem;
color: var(--red-festive);
}
.sf-user-badges {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 12px;
align-items: center;
}
/* Seal Style Tags */
.sf-badge {
font-family: var(--font-heading);
font-size: 0.75rem;
font-weight: bold;
padding: 2px 10px;
border: 1.5px solid var(--red-festive);
position: relative;
display: inline-block;
margin-right: 6px;
margin-bottom: 4px;
background: #fff;
color: var(--red-festive);
line-height: 1.2;
width: fit-content;
}
.sf-badge.mbti {
color: var(--red-festive);
border: 1px solid var(--red-festive);
opacity: 0.8;
}
.sf-badge.sf-title {
color: #fff;
background: var(--red-festive);
border-color: var(--red-festive);
box-shadow: 3px 3px 0 rgba(255, 202, 40, 0.4);
}
.sf-badge::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border: 1px solid currentColor;
opacity: 0.2;
}
.sf-user-reason {
font-size: 1.05rem;
color: var(--text-main);
line-height: 1.6;
text-align: justify;
}
/* Vertical Flow Elements */
.sf-user-masonry {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 25px;
}
.sf-quote-timeline {
display: flex;
flex-direction: column;
gap: 40px;
}
.sf-quote-item {
width: 85%;
display: flex;
gap: 20px;
position: relative;
}
.sf-quote-item.sf-right {
align-self: flex-end;
flex-direction: row-reverse;
}
.sf-quote-avatar-wrap {
width: 70px;
height: 70px;
flex-shrink: 0;
border-radius: 50%;
border: 3px solid var(--gold-primary);
background: #fff;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.sf-quote-bubble {
flex: 1;
background: #fff;
border: 2px solid var(--red-festive);
padding: 25px;
border-radius: 4px 25px 25px 25px;
position: relative;
box-shadow: 6px 6px 0 rgba(183, 28, 28, 0.1);
background-image: radial-gradient(rgba(183, 28, 28, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
}
.sf-quote-item.sf-right .sf-quote-bubble {
border-radius: 25px 4px 25px 25px;
background-color: #fffaf0;
text-align: right;
}
.sf-quote-content {
font-family: var(--font-heading);
font-size: 1.5rem;
color: var(--text-main);
margin-bottom: 12px;
line-height: 1.5;
}
.sf-quote-author {
font-family: var(--font-calligraphy);
font-size: 1.8rem;
color: var(--red-festive);
margin-bottom: 10px;
}
.sf-quote-reason {
font-size: 0.9rem;
color: #888;
border-top: 1px solid #eee;
padding-top: 10px;
text-align: left;
}
/* Footer */
footer {
margin-top: 60px;
text-align: center;
color: var(--red-festive);
font-family: var(--font-heading);
border-top: 2px solid var(--gold-primary);
padding-top: 30px;
}
.sf-footer-tag {
background: var(--gold-primary);
color: var(--red-festive);
padding: 5px 20px;
display: inline-block;
margin-top: 10px;
transform: rotate(-2deg);
}
.avoid-break {
page-break-inside: avoid;
}
</style>
</head>
<body>
<div class="container">
<div class="corner corner-tl"></div>
<div class="corner corner-tr"></div>
<div class="corner corner-bl"></div>
<div class="corner corner-br"></div>
<div class="sf-header">
<div class="sf-title-wrap">
<h1>🧧 春节特供日报 🧧</h1>
</div>
<div class="sf-date">—— {{current_date}} · 岁在丙午 ——</div>
</div>
<div class="sf-stats-wrapper">
<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">{{emoji_count}}</div>
</div>
<div class="sf-hongbao">
<div class="hb-label">总字符数</div>
<div class="hb-value" style="font-size: 1.8rem;">{{total_characters}}</div>
</div>
</div>
<div class="sf-peak-badge">
<div
style="font-family: var(--font-calligraphy); font-size: 2rem; color: var(--red-festive); margin-bottom: 5px;">
巅峰时刻
</div>
<div
style="font-family: var(--font-heading); font-size: 3.2rem; color: var(--wood-dark); font-weight: 800; line-height: 1;">
{{most_active_period}}
</div>
<div style="font-size: 0.9rem; margin-top: 12px; font-weight: bold; color: var(--red-festive);">🏮 鸿运齐天
· 众友云集 🏮</div>
</div>
</div>
<div class="sf-main-flow">
<!-- 0. Chat Quality Analysis -->
{% if chat_quality_html %}
<div class="sf-section-box">
<div class="sf-section-title"><span>🏺</span> 聊天质量锐评</div>
{{ chat_quality_html | safe }}
</div>
{% endif %}
<!-- 1. Topics: Full Width -->
{% if topics_html %}
<div class="sf-section-box">
<div class="sf-section-title"><span>&#127982;</span> 核心话题回顾</div>
<div class="sf-topic-list">
{{topics_html | safe}}
</div>
</div>
{% endif %}
<!-- 2. Chart: Full Width with SF window -->
<div class="sf-section-box">
<div class="sf-section-title"><span>📈</span> 活跃时序图</div>
<div class="sf-window">
<div class="sf-window-title">ACTIVITY CHRONICLE</div>
{{hourly_chart_html | safe}}
</div>
</div>
<!-- 3. Characters: Masonry 2-Column Grid -->
{% if titles_html %}
<div class="sf-section-box">
<div class="sf-section-title"><span>🎖️</span> 群友风云榜</div>
{{titles_html | safe}}
</div>
{% endif %}
<!-- 4. Quotes: Staggered Timeline -->
{% if quotes_html %}
<div class="sf-section-box">
<div class="sf-section-title"><span></span> 每日金句回响</div>
<div class="sf-quote-timeline">
{{quotes_html | safe}}
</div>
</div>
{% endif %}
</div>
<footer>
<div>宜 · 总结分析 // {{current_datetime}}</div>
<div style="font-size: 0.85rem; margin-top: 8px; opacity: 0.7;">Token 消耗:{{total_tokens}} (Prompt: {{prompt_tokens}}, Completion: {{completion_tokens}})</div>
<div class="sf-footer-tag">SXP-Simon / astrbot_plugin_qq_group_daily_analysis</div>
</footer>
</div>
</body>
</html>