fix: resolve code review issues - config integration, thread safety, cleanup

- Remove duplicate @staticmethod decorator on _take_screenshot
- Wire up _conf_schema.json config items to actual code:
  - max_content_length (was hardcoded 400)
  - screenshot_timeout (was hardcoded 30000/20000ms)
- Remove unused StealthyFetcher import and dead code (StealthyFetcher.adaptive=True)
- Fix _stats thread safety with threading.Lock
- Fix metadata.yaml author field (was plugin name, now 'RainySY')
- Sync README: correct screenshot size, remove non-existent screenshot_width config,
  fix asyncio.to_thread() -> run_in_executor()
- Add MIT LICENSE file
- Explicitly declare lxml>=5.0 in requirements.txt
This commit is contained in:
RainySY
2026-06-15 19:28:45 +08:00
parent 225d26d206
commit a13be98c26
5 changed files with 48 additions and 24 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 RainySY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -6,8 +6,8 @@
## ✨ 功能 ## ✨ 功能
- 🔗 **自动检测** — 聊天中出现 `linux.do` 链接立即触发 - 🔗 **自动检测** — 聊天中出现 `linux.do` 链接立即触发
- 🛡️ **绕过 Cloudflare** — 使用 [Scrapling](https://github.com/D4Vinci/Scrapling) 的 StealthyFetcher 自动解 Turnstile - 🛡️ **绕过 Cloudflare** — 使用 [Scrapling](https://github.com/D4Vinci/Scrapling) 的 StealthySession 自动解 Turnstile
- 📸 **截图预览** — 全页面截图1920×1080 - 📸 **截图预览** — 全页面截图1280×1024
- 📝 **内容摘要** — 提取标题 + 正文前 400 字 - 📝 **内容摘要** — 提取标题 + 正文前 400 字
-**异步非阻塞** — Scrapling 在独立线程池运行,不阻塞 AstrBot 主循环 -**异步非阻塞** — Scrapling 在独立线程池运行,不阻塞 AstrBot 主循环
- 💾 **缓存机制** — 30 分钟内相同链接直接返回缓存截图 - 💾 **缓存机制** — 30 分钟内相同链接直接返回缓存截图
@@ -66,7 +66,7 @@ https://linux.do/t/topic/1378383
│ linux.do/xx │ │ 事件监听器 │ │ linux.do/xx │ │ 事件监听器 │
└─────────────┘ └──────┬───────┘ └─────────────┘ └──────┬───────┘
asyncio.to_thread() run_in_executor()
┌───────▼────────┐ ┌───────▼────────┐
│ Thread Pool │ │ Thread Pool │
@@ -91,11 +91,13 @@ https://linux.do/t/topic/1378383
## ⚙️ 配置 ## ⚙️ 配置
通过 `_conf_schema.json` 支持以下配置(可选) 通过 `_conf_schema.json` 支持以下配置:
- `cache_ttl`: 缓存有效期(秒,默认 1800 | 配置项 | 说明 | 默认值 |
- `screenshot_width`: 截图宽度(默认 1920 |--------|------|--------|
- `max_content_length`: 内容摘要最大长度(默认 400 | `cache_ttl` | 缓存有效期(秒),设为 0 关闭缓存 | 1800 |
| `max_content_length` | 内容摘要最大长度(字符) | 400 |
| `screenshot_timeout` | 截图超时(秒) | 15 |
## ⚠️ 注意事项 ## ⚠️ 注意事项

24
main.py
View File

@@ -13,6 +13,7 @@ import hashlib
from pathlib import Path from pathlib import Path
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import html as html_mod import html as html_mod
import threading
from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
@@ -22,12 +23,11 @@ from astrbot.api import logger
from astrbot.api import AstrBotConfig from astrbot.api import AstrBotConfig
try: try:
from scrapling.fetchers import StealthyFetcher, StealthySession as _StealthySession from scrapling.fetchers import StealthySession as _StealthySession
from lxml import html as _lh from lxml import html as _lh
SCRAPLING_AVAILABLE = True SCRAPLING_AVAILABLE = True
except ImportError: except ImportError:
SCRAPLING_AVAILABLE = False SCRAPLING_AVAILABLE = False
StealthyFetcher = None
_StealthySession = None _StealthySession = None
_lh = None _lh = None
@@ -53,6 +53,7 @@ class LinuxDoPreviewPlugin(Star):
) )
self._stats = {"total": 0, "cache_hit": 0, "error": 0} self._stats = {"total": 0, "cache_hit": 0, "error": 0}
self._stats_lock = threading.Lock()
async def terminate(self): async def terminate(self):
_EXECUTOR.shutdown(wait=False) _EXECUTOR.shutdown(wait=False)
@@ -94,9 +95,11 @@ class LinuxDoPreviewPlugin(Star):
if summary: if summary:
yield event.plain_result(summary) yield event.plain_result(summary)
with self._stats_lock:
self._stats["total"] += 1 self._stats["total"] += 1
except Exception as e: except Exception as e:
with self._stats_lock:
self._stats["error"] += 1 self._stats["error"] += 1
logger.error(f"[LinuxDoPreview] 预览失败: {type(e).__name__}: {e}") logger.error(f"[LinuxDoPreview] 预览失败: {type(e).__name__}: {e}")
yield event.plain_result(f"❌ 预览获取失败: {str(e)[:200]}") yield event.plain_result(f"❌ 预览获取失败: {str(e)[:200]}")
@@ -130,8 +133,6 @@ class LinuxDoPreviewPlugin(Star):
and sz > 50 * 1024 # 小于 50KB 的截图视为无效(黑屏/空白) and sz > 50 * 1024 # 小于 50KB 的截图视为无效(黑屏/空白)
) )
StealthyFetcher.adaptive = True # type: ignore[union-attr]
with _StealthySession( # type: ignore[union-attr] with _StealthySession( # type: ignore[union-attr]
headless=True, solve_cloudflare=True headless=True, solve_cloudflare=True
) as session: ) as session:
@@ -148,6 +149,7 @@ class LinuxDoPreviewPlugin(Star):
session, url, screenshot_path session, url, screenshot_path
) )
else: else:
with self._stats_lock:
self._stats["cache_hit"] += 1 self._stats["cache_hit"] += 1
logger.info( logger.info(
f"[LinuxDoPreview] 使用缓存截图: {screenshot_path.name}" f"[LinuxDoPreview] 使用缓存截图: {screenshot_path.name}"
@@ -158,10 +160,9 @@ class LinuxDoPreviewPlugin(Star):
# ─────────── 截图(复用 StealthySession 的浏览器上下文) ─────────── # ─────────── 截图(复用 StealthySession 的浏览器上下文) ───────────
@staticmethod def _take_screenshot(self, session, url: str, save_path: Path) -> Path | None:
@staticmethod
def _take_screenshot(session, url: str, save_path: Path) -> Path | None:
"""在已有 cf_clearance 的上下文中新建标签页截图""" """在已有 cf_clearance 的上下文中新建标签页截图"""
timeout_ms = self.config.get("screenshot_timeout", 15) * 1000
try: try:
ctx = session.context ctx = session.context
if not ctx: if not ctx:
@@ -171,13 +172,13 @@ class LinuxDoPreviewPlugin(Star):
page.set_viewport_size({"width": 1280, "height": 1024}) page.set_viewport_size({"width": 1280, "height": 1024})
# 导航(已有 cf_clearance cookie不应再触发 Cloudflare # 导航(已有 cf_clearance cookie不应再触发 Cloudflare
page.goto(url, wait_until="load", timeout=30000) page.goto(url, wait_until="load", timeout=timeout_ms)
page.wait_for_timeout(3000) page.wait_for_timeout(3000)
page.screenshot( page.screenshot(
path=str(save_path), path=str(save_path),
full_page=True, full_page=True,
timeout=20000, timeout=timeout_ms,
) )
sz = save_path.stat().st_size sz = save_path.stat().st_size
logger.info( logger.info(
@@ -238,12 +239,11 @@ class LinuxDoPreviewPlugin(Star):
break break
return "\n\n".join(parts) return "\n\n".join(parts)
@staticmethod def _build_summary(self, title: str, content: str, url: str) -> str:
def _build_summary(title: str, content: str, url: str) -> str:
lines = [f"📌 {title}"] lines = [f"📌 {title}"]
if content: if content:
lines.append("") lines.append("")
max_len = 400 max_len = self.config.get("max_content_length", 400)
lines.append(content[:max_len]) lines.append(content[:max_len])
if len(content) > max_len: if len(content) > max_len:
lines[-1] += "" lines[-1] += ""

View File

@@ -2,10 +2,10 @@ name: astrbot_plugin_linuxdo
display_name: LinuxDo Preview display_name: LinuxDo Preview
short_desc: 自动检测 linux.do 链接,绕过 Cloudflare 截图发送预览 short_desc: 自动检测 linux.do 链接,绕过 Cloudflare 截图发送预览
desc: > desc: >
自动检测聊天消息中的 linux.do 链接,使用 Scrapling 的 StealthyFetcher 自动检测聊天消息中的 linux.do 链接,使用 Scrapling 的 StealthySession
绕过 Cloudflare Turnstile 防护,获取页面截图和内容摘要并发送预览。 绕过 Cloudflare Turnstile 防护,获取页面截图和内容摘要并发送预览。
支持缓存避免重复请求,异步非阻塞设计。 支持缓存避免重复请求,异步非阻塞设计。
author: astrbot_plugin_linuxdo author: RainySY
version: 1.0.0 version: 1.0.0
repo: https://github.com/sakuradairong/astrbot_plugin_linuxdo repo: https://github.com/sakuradairong/astrbot_plugin_linuxdo
astrbot_version: ">=4.16" astrbot_version: ">=4.16"

View File

@@ -1 +1,2 @@
scrapling[fetchers]>=0.4 scrapling[fetchers]>=0.4
lxml>=5.0