mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
fix: 正确显示话题分析头像
This commit is contained in:
@@ -608,7 +608,19 @@ class TelegramAdapter(PlatformAdapter):
|
|||||||
if file.file_path:
|
if file.file_path:
|
||||||
# 构建完整 URL
|
# 构建完整 URL
|
||||||
# 格式: https://api.telegram.org/file/bot<token>/<file_path>
|
# 格式: https://api.telegram.org/file/bot<token>/<file_path>
|
||||||
return file.file_path
|
# python-telegram-bot 的 File.file_path 属性通常只返回路径部分
|
||||||
|
# 需要手动拼接或使用 instance.file.file_path (取决于版本)
|
||||||
|
|
||||||
|
file_path = file.file_path
|
||||||
|
if file_path.startswith("http"):
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
# 尝试构建完整 URL
|
||||||
|
if hasattr(client, "token"):
|
||||||
|
return f"https://api.telegram.org/file/bot{client.token}/{file_path}"
|
||||||
|
|
||||||
|
# 如果无法获取 token,返回 None
|
||||||
|
return None
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"[Telegram] 获取用户头像失败: {e}")
|
logger.debug(f"[Telegram] 获取用户头像失败: {e}")
|
||||||
@@ -640,7 +652,14 @@ class TelegramAdapter(PlatformAdapter):
|
|||||||
if chat.photo:
|
if chat.photo:
|
||||||
file = await client.get_file(chat.photo.big_file_id)
|
file = await client.get_file(chat.photo.big_file_id)
|
||||||
if file.file_path:
|
if file.file_path:
|
||||||
return file.file_path
|
file_path = file.file_path
|
||||||
|
if file_path.startswith("http"):
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
if hasattr(client, "token"):
|
||||||
|
return f"https://api.telegram.org/file/bot{client.token}/{file_path}"
|
||||||
|
|
||||||
|
return None
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"[Telegram] 获取群头像失败: {e}")
|
logger.debug(f"[Telegram] 获取群头像失败: {e}")
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ class ReportGenerator(IReportGenerator):
|
|||||||
if isinstance(image_data, bytes):
|
if isinstance(image_data, bytes):
|
||||||
b64 = base64.b64encode(image_data).decode("utf-8")
|
b64 = base64.b64encode(image_data).decode("utf-8")
|
||||||
image_url = f"base64://{b64}"
|
image_url = f"base64://{b64}"
|
||||||
logger.info(f"图片生成成功 ({image_options}): [Base64 Data {len(image_data)} bytes]")
|
logger.info(
|
||||||
|
f"图片生成成功 ({image_options}): [Base64 Data {len(image_data)} bytes]"
|
||||||
|
)
|
||||||
return image_url, html_content
|
return image_url, html_content
|
||||||
elif isinstance(image_data, str):
|
elif isinstance(image_data, str):
|
||||||
# Fallback: 如果返回的是字符串(可能是URL或路径)
|
# Fallback: 如果返回的是字符串(可能是URL或路径)
|
||||||
@@ -129,6 +131,7 @@ class ReportGenerator(IReportGenerator):
|
|||||||
if not image_data.startswith(("http://", "https://")):
|
if not image_data.startswith(("http://", "https://")):
|
||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if os.path.exists(image_data):
|
if os.path.exists(image_data):
|
||||||
with open(image_data, "rb") as f:
|
with open(image_data, "rb") as f:
|
||||||
file_bytes = f.read()
|
file_bytes = f.read()
|
||||||
@@ -137,23 +140,38 @@ class ReportGenerator(IReportGenerator):
|
|||||||
is_valid_image = False
|
is_valid_image = False
|
||||||
if file_bytes.startswith(b"\xff\xd8"): # JPEG
|
if file_bytes.startswith(b"\xff\xd8"): # JPEG
|
||||||
is_valid_image = True
|
is_valid_image = True
|
||||||
elif file_bytes.startswith(b"\x89PNG\r\n\x1a\n"): # PNG
|
elif file_bytes.startswith(
|
||||||
|
b"\x89PNG\r\n\x1a\n"
|
||||||
|
): # PNG
|
||||||
is_valid_image = True
|
is_valid_image = True
|
||||||
|
|
||||||
if not is_valid_image:
|
if not is_valid_image:
|
||||||
try:
|
try:
|
||||||
text_content = file_bytes.decode('utf-8')
|
text_content = file_bytes.decode(
|
||||||
if "Error" in text_content or "Exception" in text_content:
|
"utf-8"
|
||||||
logger.error(f"渲染器生成了错误文件而非图片: {text_content[:200]}")
|
)
|
||||||
|
if (
|
||||||
|
"Error" in text_content
|
||||||
|
or "Exception" in text_content
|
||||||
|
):
|
||||||
|
logger.error(
|
||||||
|
f"渲染器生成了错误文件而非图片: {text_content[:200]}"
|
||||||
|
)
|
||||||
return None, html_content
|
return None, html_content
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.warning(f"生成的图片文件头异常 (非JPEG/PNG): {file_bytes[:10].hex()}")
|
logger.warning(
|
||||||
|
f"生成的图片文件头异常 (非JPEG/PNG): {file_bytes[:10].hex()}"
|
||||||
|
)
|
||||||
return None, html_content
|
return None, html_content
|
||||||
|
|
||||||
b64 = base64.b64encode(file_bytes).decode("utf-8")
|
b64 = base64.b64encode(file_bytes).decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
image_url = f"base64://{b64}"
|
image_url = f"base64://{b64}"
|
||||||
logger.info(f"本地图片转 Base64 成功: {len(file_bytes)} bytes")
|
logger.info(
|
||||||
|
f"本地图片转 Base64 成功: {len(file_bytes)} bytes"
|
||||||
|
)
|
||||||
return image_url, html_content
|
return image_url, html_content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"尝试读取本地图片失败: {e}")
|
logger.warning(f"尝试读取本地图片失败: {e}")
|
||||||
@@ -415,13 +433,15 @@ class ReportGenerator(IReportGenerator):
|
|||||||
"vertical-align:middle;border:1px solid rgba(0,0,0,0.1);text-decoration:none;"
|
"vertical-align:middle;border:1px solid rgba(0,0,0,0.1);text-decoration:none;"
|
||||||
)
|
)
|
||||||
img_style = "width:18px;height:18px;border-radius:50%;margin-right:4px;display:block;"
|
img_style = "width:18px;height:18px;border-radius:50%;margin-right:4px;display:block;"
|
||||||
name_style = "font-size:0.85em;color:inherit;font-weight:500;line-height:1;"
|
name_style = (
|
||||||
|
"font-size:0.85em;color:inherit;font-weight:500;line-height:1;"
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f'<span class="user-capsule" style="{capsule_style}">'
|
f'<span class="user-capsule" style="{capsule_style}">'
|
||||||
f'<img src="{url}" style="{img_style}">'
|
f'<img src="{url}" style="{img_style}">'
|
||||||
f'<span style="{name_style}">{name}</span>'
|
f'<span style="{name_style}">{name}</span>'
|
||||||
f'</span>'
|
f"</span>"
|
||||||
)
|
)
|
||||||
elif url:
|
elif url:
|
||||||
# 仅有头像回退
|
# 仅有头像回退
|
||||||
@@ -455,19 +475,24 @@ class ReportGenerator(IReportGenerator):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _get_user_avatar(self, user_id: str, avatar_getter=None) -> str | None:
|
async def _get_user_avatar(self, user_id: str, avatar_getter=None) -> str:
|
||||||
"""
|
"""
|
||||||
获取用户头像的本地文件路径 (file:// URI)。
|
获取用户头像的 Base64 Data URI。
|
||||||
|
|
||||||
优化策略:
|
策略:
|
||||||
1. 优先使用本地缓存文件,避免重复下载和 Base64 编解码开销
|
1. 优先使用本地缓存文件
|
||||||
2. 下载后保存到 data/temp/avatars/ 目录
|
2. 下载并保存到 data/plugin_data/.../cache/avatars/
|
||||||
3. 返回 file:// 协议路径,供 Playwright 本地渲染使用
|
3. 读取文件并转换为 Base64,嵌入 HTML
|
||||||
|
这是为了解决 Docker/沙箱环境中渲染器无法访问宿主机 file:// 路径的问题。
|
||||||
"""
|
"""
|
||||||
|
import base64
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 准备缓存目录
|
# 1. 准备缓存目录
|
||||||
# 使用 plugin_data 目录以确保持久化和标准结构
|
# 使用 plugin_data 目录以确保持久化和标准结构
|
||||||
temp_dir = Path("data/plugin_data/astrbot_plugin_qq_group_daily_analysis/cache/avatars")
|
temp_dir = Path(
|
||||||
|
"data/plugin_data/astrbot_plugin_qq_group_daily_analysis/cache/avatars"
|
||||||
|
)
|
||||||
if not temp_dir.exists():
|
if not temp_dir.exists():
|
||||||
await asyncio.to_thread(temp_dir.mkdir, parents=True, exist_ok=True)
|
await asyncio.to_thread(temp_dir.mkdir, parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -475,12 +500,18 @@ class ReportGenerator(IReportGenerator):
|
|||||||
file_name = f"{user_id}_40.jpg"
|
file_name = f"{user_id}_40.jpg"
|
||||||
file_path = temp_dir / file_name
|
file_path = temp_dir / file_name
|
||||||
|
|
||||||
|
file_content = None
|
||||||
|
|
||||||
# 2. 检查缓存
|
# 2. 检查缓存
|
||||||
if file_path.exists() and file_path.stat().st_size > 0:
|
if file_path.exists() and file_path.stat().st_size > 0:
|
||||||
# 必须使用绝对路径
|
# 异步读取缓存
|
||||||
return f"file://{file_path.absolute()}"
|
try:
|
||||||
|
file_content = await asyncio.to_thread(file_path.read_bytes)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 3. 获取 URL
|
# 3. 如果无缓存,获取 URL 并下载
|
||||||
|
if not file_content:
|
||||||
avatar_url = None
|
avatar_url = None
|
||||||
if avatar_getter:
|
if avatar_getter:
|
||||||
try:
|
try:
|
||||||
@@ -491,29 +522,84 @@ class ReportGenerator(IReportGenerator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"使用 custom avatar_getter 获取头像失败: {e}")
|
logger.warning(f"使用 custom avatar_getter 获取头像失败: {e}")
|
||||||
|
|
||||||
# 4. Fallback URL
|
# 4. Fallback URL (仅针对看起来像 QQ 号的 ID)
|
||||||
if not avatar_url:
|
if not avatar_url:
|
||||||
if user_id.isdigit() and 5 <= len(user_id) <= 12:
|
if user_id.isdigit() and 5 <= len(user_id) <= 12:
|
||||||
# 强制使用 spec=40
|
# 强制使用 spec=40
|
||||||
avatar_url = f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=40"
|
avatar_url = (
|
||||||
|
f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=40"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return None
|
# 其他平台若无 URL,无法获取头像
|
||||||
|
return self._get_default_avatar_base64()
|
||||||
|
|
||||||
# 5. 下载并保存
|
# 5. 下载并保存
|
||||||
async with aiohttp.ClientSession() as client:
|
async with aiohttp.ClientSession() as client:
|
||||||
|
try:
|
||||||
async with client.get(avatar_url, timeout=5) as response:
|
async with client.get(avatar_url, timeout=5) as response:
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
content = await response.read()
|
content = await response.read()
|
||||||
if content:
|
if content:
|
||||||
await asyncio.to_thread(file_path.write_bytes, content)
|
# 校验文件头
|
||||||
return f"file://{file_path.absolute()}"
|
is_valid_image = False
|
||||||
|
if content.startswith(b"\xff\xd8"): # JPEG
|
||||||
|
is_valid_image = True
|
||||||
|
elif content.startswith(
|
||||||
|
b"\x89PNG\r\n\x1a\n"
|
||||||
|
): # PNG
|
||||||
|
is_valid_image = True
|
||||||
|
elif content.startswith(b"GIF8"): # GIF
|
||||||
|
is_valid_image = True
|
||||||
|
elif (
|
||||||
|
content.startswith(b"RIFF")
|
||||||
|
and b"WEBP" in content[:16]
|
||||||
|
): # WebP
|
||||||
|
is_valid_image = True
|
||||||
|
|
||||||
|
if is_valid_image:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
file_path.write_bytes, content
|
||||||
|
)
|
||||||
|
file_content = content
|
||||||
else:
|
else:
|
||||||
logger.warning(f"下载头像失败 {avatar_url}: {response.status}")
|
logger.warning(
|
||||||
|
f"下载的头像数据格式无效 ({avatar_url})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"下载头像失败 {avatar_url}: {response.status}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"下载头像网络错误 {avatar_url}: {e}")
|
||||||
|
|
||||||
|
# 6. 转换为 Base64 Data URI
|
||||||
|
if file_content:
|
||||||
|
b64 = base64.b64encode(file_content).decode("utf-8")
|
||||||
|
# 简单判断 mime type
|
||||||
|
mime = "image/jpeg"
|
||||||
|
if file_content.startswith(b"\x89PNG"):
|
||||||
|
mime = "image/png"
|
||||||
|
elif file_content.startswith(b"GIF8"):
|
||||||
|
mime = "image/gif"
|
||||||
|
elif file_content.startswith(b"RIFF"):
|
||||||
|
mime = "image/webp"
|
||||||
|
|
||||||
|
return f"data:{mime};base64,{b64}"
|
||||||
|
|
||||||
|
return self._get_default_avatar_base64()
|
||||||
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取用户头像失败 {user_id}: {e}")
|
logger.error(f"获取用户头像失败 {user_id}: {e}")
|
||||||
return None
|
return self._get_default_avatar_base64()
|
||||||
|
|
||||||
|
def _get_default_avatar_base64(self) -> str:
|
||||||
|
"""返回默认头像 (灰色圆形占位符)"""
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# 一个简单的灰色圆圈 SVG 转 Base64
|
||||||
|
svg = '<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50" fill="#ddd"/></svg>'
|
||||||
|
b64 = base64.b64encode(svg.encode("utf-8")).decode("utf-8")
|
||||||
|
return f"data:image/svg+xml;base64,{b64}"
|
||||||
|
|
||||||
async def _html_to_pdf(self, html_content: str, output_path: str) -> bool:
|
async def _html_to_pdf(self, html_content: str, output_path: str) -> bool:
|
||||||
"""将 HTML 内容转换为 PDF 文件"""
|
"""将 HTML 内容转换为 PDF 文件"""
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="topic-title">{{ topic.topic.topic }}</span>
|
<span class="topic-title">{{ topic.topic.topic }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="topic-contributors">参与者: {{ topic.contributors }}</div>
|
<div class="topic-contributors">参与者: {{ topic.contributors }}</div>
|
||||||
<div class="topic-detail">{{ topic.detail }}</div>
|
<div class="topic-detail">{{ topic.detail | safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="topic-content">
|
<div class="topic-content">
|
||||||
<h3 class="topic-title">{{ topic.topic.topic }}</h3>
|
<h3 class="topic-title">{{ topic.topic.topic }}</h3>
|
||||||
<p class="topic-meta">PARTICIPANTS: {{ topic.contributors }}</p>
|
<p class="topic-meta">PARTICIPANTS: {{ topic.contributors }}</p>
|
||||||
<p class="topic-detail">{{ topic.detail }}</p>
|
<p class="topic-detail">{{ topic.detail | safe }}</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
style="font-family: var(--font-hand); font-size: 1.05em; color: var(--ink-secondary); margin-bottom: 8px;">
|
style="font-family: var(--font-hand); font-size: 1.05em; color: var(--ink-secondary); margin-bottom: 8px;">
|
||||||
🙋♀️ 参与者: {{ topic.contributors }}
|
🙋♀️ 参与者: {{ topic.contributors }}
|
||||||
</div>
|
</div>
|
||||||
<div class="topic-detail">{{ topic.detail }}</div>
|
<div class="topic-detail">{{ topic.detail | safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div style="margin-bottom: 10px; border-bottom: 1px dashed #ccc; padding-bottom: 5px;">
|
<div style="margin-bottom: 10px; border-bottom: 1px dashed #ccc; padding-bottom: 5px;">
|
||||||
<strong>#{{ item.index }} {{ item.topic.topic }}</strong>
|
<strong>#{{ item.index }} {{ item.topic.topic }}</strong>
|
||||||
<span style="color: #666; font-size: 0.8em;">(参与者: {{ item.contributors }})</span>
|
<span style="color: #666; font-size: 0.8em;">(参与者: {{ item.contributors }})</span>
|
||||||
<p style="margin: 5px 0;">{{ item.detail }}</p>
|
<p style="margin: 5px 0;">{{ item.detail | safe }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user