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:
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
16
README.md
16
README.md
@@ -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
24
main.py
@@ -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] += "…"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
scrapling[fetchers]>=0.4
|
scrapling[fetchers]>=0.4
|
||||||
|
lxml>=5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user