nonebot-plugin-htmlrender 0.4.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.github/workflows/publish.yml +6 -1
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/PKG-INFO +2 -2
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/__init__.py +3 -3
- nonebot_plugin_htmlrender-0.5.1/nonebot_plugin_htmlrender/browser.py +356 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/config.py +6 -1
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/pyproject.toml +29 -14
- nonebot_plugin_htmlrender-0.5.1/tests/conftest.py +15 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/uv.lock +5 -5
- nonebot_plugin_htmlrender-0.4.0/nonebot_plugin_htmlrender/browser.py +0 -131
- nonebot_plugin_htmlrender-0.4.0/tests/conftest.py +0 -10
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.flake8 +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.github/workflows/codecov.yml +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.gitignore +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.python-version +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/LICENSE +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/README.md +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/example.md +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/html2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/md2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/template2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/text2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/.env.dev +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/bot.py +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/html2pic.html +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/data_source.py +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/text.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/test_htmlrender.py +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/test_template_filter.png +0 -0
- {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/utils.py +0 -0
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.github/workflows/publish.yml
RENAMED
|
@@ -43,8 +43,13 @@ jobs:
|
|
|
43
43
|
- name: Build Package
|
|
44
44
|
run: uv build
|
|
45
45
|
|
|
46
|
+
- uses: actions/upload-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: artifact
|
|
49
|
+
path: dist/ # or path/to/artifact
|
|
50
|
+
|
|
46
51
|
- name: pypi-publish
|
|
47
|
-
uses: pypa/gh-action-pypi-publish@v1.
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@v1.12.3
|
|
48
53
|
|
|
49
54
|
# - name: Publish Package to PyPI
|
|
50
55
|
# run: uv publish
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-htmlrender
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: 通过浏览器渲染图片
|
|
5
5
|
Project-URL: Homepage, https://github.com/kexue-z/nonebot-plugin-htmlrender
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/kexue-z/nonebot-plugin-htmlrender/issues
|
|
@@ -6,6 +6,7 @@ from .browser import (
|
|
|
6
6
|
get_browser as get_browser,
|
|
7
7
|
get_new_page as get_new_page,
|
|
8
8
|
shutdown_browser as shutdown_browser,
|
|
9
|
+
init_browser as init_browser,
|
|
9
10
|
)
|
|
10
11
|
from .data_source import (
|
|
11
12
|
capture_element as capture_element,
|
|
@@ -35,9 +36,8 @@ async def init(**kwargs):
|
|
|
35
36
|
Returns:
|
|
36
37
|
Browser: Browser
|
|
37
38
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return browser
|
|
39
|
+
logger.info("Browser Starting.")
|
|
40
|
+
return await init_browser(**kwargs)
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
@driver.on_shutdown
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
import sys
|
|
5
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional, Literal, AsyncIterator, TextIO
|
|
9
|
+
from typing_extensions import TypeGuard
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from nonebot.log import logger
|
|
13
|
+
from playwright.async_api import Browser, Error, Page, Playwright, async_playwright, BrowserType
|
|
14
|
+
|
|
15
|
+
from .config import plugin_config
|
|
16
|
+
|
|
17
|
+
_browser: Optional[Browser] = None
|
|
18
|
+
_playwright: Optional[Playwright] = None
|
|
19
|
+
AllowedBrowser = Literal["chromium", "firefox", "webkit"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def _suppress_and_log():
|
|
24
|
+
"""抑制并记录关闭期间的任何异常。"""
|
|
25
|
+
try:
|
|
26
|
+
yield
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.opt(exception=e).warning("关闭 playwright 时发生错误。")
|
|
29
|
+
|
|
30
|
+
def is_allowed_browser(browser_type: str) -> TypeGuard[AllowedBrowser]:
|
|
31
|
+
"""检查浏览器类型是否有效。"""
|
|
32
|
+
return browser_type in ["chromium", "firefox", "webkit"]
|
|
33
|
+
|
|
34
|
+
async def _launch(browser_type: AllowedBrowser, **kwargs) -> Browser:
|
|
35
|
+
logger.info(f"使用 {browser_type.capitalize()} 启动中...")
|
|
36
|
+
_browser_cls: BrowserType = getattr(_playwright, browser_type)
|
|
37
|
+
_browser = await _browser_cls.launch(**kwargs)
|
|
38
|
+
logger.debug(f"{browser_type.capitalize()} 路径: {_browser_cls.executable_path}")
|
|
39
|
+
return _browser
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def init_browser(**kwargs) -> Browser:
|
|
43
|
+
"""初始化全局 playwright 浏览器实例并返回浏览器实例。"""
|
|
44
|
+
try:
|
|
45
|
+
return await start_browser(**kwargs)
|
|
46
|
+
except Error:
|
|
47
|
+
await install_browser()
|
|
48
|
+
return await start_browser(**kwargs)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def start_browser(**kwargs) -> Browser:
|
|
52
|
+
"""
|
|
53
|
+
启动 playwright 浏览器实例。
|
|
54
|
+
Args:
|
|
55
|
+
**kwargs (Any): 传递给 `playwright.launch` 的关键字参数。
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Browser: 浏览器实例。
|
|
59
|
+
"""
|
|
60
|
+
global _browser, _playwright
|
|
61
|
+
browser_type = (
|
|
62
|
+
plugin_config.htmlrender_browser.lower()
|
|
63
|
+
if plugin_config.htmlrender_browser
|
|
64
|
+
else "chromium"
|
|
65
|
+
)
|
|
66
|
+
if not is_allowed_browser(browser_type):
|
|
67
|
+
raise ValueError(f"不支持的浏览器类型: {browser_type}")
|
|
68
|
+
|
|
69
|
+
if plugin_config.htmlrender_browser_channel:
|
|
70
|
+
kwargs["channel"] = plugin_config.htmlrender_browser_channel
|
|
71
|
+
|
|
72
|
+
if plugin_config.htmlrender_proxy_host:
|
|
73
|
+
kwargs["proxy"] = {
|
|
74
|
+
"server": plugin_config.htmlrender_proxy_host,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_playwright = await async_playwright().start()
|
|
78
|
+
|
|
79
|
+
if browser_type == "chromium" and plugin_config.htmlrender_connect_over_cdp:
|
|
80
|
+
logger.info("使用 Chromium CDP 连接中...")
|
|
81
|
+
_browser = await _playwright.chromium.connect_over_cdp(**kwargs)
|
|
82
|
+
else:
|
|
83
|
+
_browser = await _launch(browser_type)
|
|
84
|
+
|
|
85
|
+
return _browser
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def get_browser(**kwargs) -> Browser:
|
|
89
|
+
"""获取 Browser 实例,不存在则初始化"""
|
|
90
|
+
if _browser and _browser.is_connected():
|
|
91
|
+
return _browser
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
return await init_browser(**kwargs)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise RuntimeError("无法初始化浏览器实例") from e
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@asynccontextmanager
|
|
100
|
+
async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
|
|
101
|
+
"""
|
|
102
|
+
获取一个新的页面的上下文管理器。
|
|
103
|
+
Args:
|
|
104
|
+
device_scale_factor (float): 设备缩放因子。
|
|
105
|
+
**kwargs (Any): 传递给 `browser.new_context` 的关键字参数。
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
AsyncIterator[Page]: 页面对象。
|
|
109
|
+
"""
|
|
110
|
+
ctx = await get_browser()
|
|
111
|
+
page = await ctx.new_page(device_scale_factor=device_scale_factor, **kwargs)
|
|
112
|
+
try:
|
|
113
|
+
yield page
|
|
114
|
+
finally:
|
|
115
|
+
await page.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def shutdown_browser() -> None:
|
|
119
|
+
"""关闭浏览器实例。"""
|
|
120
|
+
if _browser:
|
|
121
|
+
with _suppress_and_log():
|
|
122
|
+
await _browser.close()
|
|
123
|
+
if _playwright:
|
|
124
|
+
with _suppress_and_log():
|
|
125
|
+
await _playwright.stop()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class MirrorSource:
|
|
130
|
+
name: str
|
|
131
|
+
url: str
|
|
132
|
+
priority: int
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def check_mirror_connectivity(
|
|
136
|
+
mirrors: list[MirrorSource], timeout: int = 5
|
|
137
|
+
) -> Optional[MirrorSource]:
|
|
138
|
+
"""
|
|
139
|
+
检查镜像源的可用性并返回最佳镜像源。
|
|
140
|
+
Args:
|
|
141
|
+
mirrors (list[MirrorSource]): 镜像源列表。
|
|
142
|
+
timeout (int): 连接超时时间。
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Optional[MirrorSource]: 最佳镜像源。
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
async def _check_single_mirror(mirror: MirrorSource) -> tuple[MirrorSource, float]:
|
|
149
|
+
"""
|
|
150
|
+
检查单个镜像源的可用性。
|
|
151
|
+
Args:
|
|
152
|
+
mirror (MirrorSource): 镜像源。
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
tuple[MirrorSource, float]: 镜像源和连接耗时。
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
parsed_url = urlparse(mirror.url)
|
|
159
|
+
host = parsed_url.hostname
|
|
160
|
+
port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
|
|
161
|
+
|
|
162
|
+
start_time = asyncio.get_event_loop().time()
|
|
163
|
+
|
|
164
|
+
_, _ = await asyncio.wait_for(
|
|
165
|
+
asyncio.open_connection(host, port), timeout=timeout
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
169
|
+
return mirror, elapsed
|
|
170
|
+
|
|
171
|
+
except (asyncio.TimeoutError, ConnectionRefusedError, socket.gaierror) as e:
|
|
172
|
+
logger.debug(f"镜像源 {mirror.name} 连接失败: {str(e)}")
|
|
173
|
+
return mirror, float("inf")
|
|
174
|
+
|
|
175
|
+
tasks = [_check_single_mirror(mirror) for mirror in mirrors]
|
|
176
|
+
results = await asyncio.gather(*tasks)
|
|
177
|
+
|
|
178
|
+
available_mirrors = [(m, t) for m, t in results if t != float("inf")]
|
|
179
|
+
if not available_mirrors:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return min(available_mirrors, key=lambda x: (x[1], -x[0].priority))[0]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@asynccontextmanager
|
|
186
|
+
async def download_host_context(mirrors: list[MirrorSource]) -> AsyncIterator[None]:
|
|
187
|
+
"""
|
|
188
|
+
为下载设置镜像源上下文管理器。
|
|
189
|
+
Args:
|
|
190
|
+
mirrors (list[MirrorSource]): 镜像源列表。
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
AsyncIterator[None]: 上下文管理器。
|
|
194
|
+
"""
|
|
195
|
+
had_original = "PLAYWRIGHT_DOWNLOAD_HOST" in os.environ
|
|
196
|
+
original_host = os.environ.get("PLAYWRIGHT_DOWNLOAD_HOST")
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
best_mirror = await check_mirror_connectivity(mirrors)
|
|
200
|
+
|
|
201
|
+
if plugin_config.htmlrender_download_host:
|
|
202
|
+
logger.info(f"使用配置的下载源: {plugin_config.htmlrender_download_host}")
|
|
203
|
+
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = (
|
|
204
|
+
plugin_config.htmlrender_download_host
|
|
205
|
+
)
|
|
206
|
+
elif best_mirror:
|
|
207
|
+
logger.info(f"使用镜像源: {best_mirror.name} ({best_mirror.url})")
|
|
208
|
+
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = best_mirror.url
|
|
209
|
+
else:
|
|
210
|
+
logger.info("无可用镜像源,使用默认源")
|
|
211
|
+
|
|
212
|
+
yield
|
|
213
|
+
finally:
|
|
214
|
+
if had_original and original_host is not None:
|
|
215
|
+
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = original_host
|
|
216
|
+
elif "PLAYWRIGHT_DOWNLOAD_HOST" in os.environ:
|
|
217
|
+
del os.environ["PLAYWRIGHT_DOWNLOAD_HOST"]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class MessageType(Enum):
|
|
221
|
+
PROGRESS = "progress"
|
|
222
|
+
DOWNLOAD = "download"
|
|
223
|
+
INFO = "info"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def read_stream(stream, out: TextIO = sys.stdout) -> None:
|
|
227
|
+
"""实时读取并输出流内容
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
stream: 要读取的流
|
|
231
|
+
out: 输出流,默认为 sys.stdout
|
|
232
|
+
"""
|
|
233
|
+
last_progress = ""
|
|
234
|
+
|
|
235
|
+
def write_progress(_: str) -> None:
|
|
236
|
+
"""写入进度条"""
|
|
237
|
+
out.write(f"\r{_}")
|
|
238
|
+
out.flush()
|
|
239
|
+
|
|
240
|
+
def ensure_newline() -> None:
|
|
241
|
+
"""确保进度条后换行"""
|
|
242
|
+
nonlocal last_progress
|
|
243
|
+
if last_progress:
|
|
244
|
+
out.write("\n")
|
|
245
|
+
out.flush()
|
|
246
|
+
last_progress = ""
|
|
247
|
+
|
|
248
|
+
def get_message_type(_: str) -> MessageType:
|
|
249
|
+
"""获取消息类型"""
|
|
250
|
+
if "|" in _ and "%" in _ and ("MiB" in _ or "KiB" in _):
|
|
251
|
+
return MessageType.PROGRESS
|
|
252
|
+
elif _.startswith("Downloading "):
|
|
253
|
+
return MessageType.DOWNLOAD
|
|
254
|
+
return MessageType.INFO
|
|
255
|
+
|
|
256
|
+
while True:
|
|
257
|
+
line = await stream.readline()
|
|
258
|
+
if not line:
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
text = line.decode().strip()
|
|
262
|
+
if not text:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
msg_type = get_message_type(text)
|
|
266
|
+
|
|
267
|
+
if msg_type is MessageType.PROGRESS:
|
|
268
|
+
progress_text = text.split("|", 1)[1].strip()
|
|
269
|
+
write_progress(progress_text)
|
|
270
|
+
last_progress = text
|
|
271
|
+
else:
|
|
272
|
+
ensure_newline()
|
|
273
|
+
logger.info(text)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def execute_install_command(
|
|
277
|
+
install_cmd: list[str], timeout: int
|
|
278
|
+
) -> tuple[bool, str]:
|
|
279
|
+
"""
|
|
280
|
+
执行安装命令并返回结果。
|
|
281
|
+
Args:
|
|
282
|
+
install_cmd (list[str]): 安装命令。
|
|
283
|
+
timeout (int): 等待超时时间。
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
tuple[bool, str]: 是否安装成功和消息。
|
|
287
|
+
"""
|
|
288
|
+
process = await asyncio.create_subprocess_exec(
|
|
289
|
+
*install_cmd,
|
|
290
|
+
stdout=asyncio.subprocess.PIPE,
|
|
291
|
+
stderr=asyncio.subprocess.PIPE,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
await asyncio.wait_for(
|
|
296
|
+
asyncio.gather(
|
|
297
|
+
read_stream(process.stdout),
|
|
298
|
+
read_stream(process.stderr),
|
|
299
|
+
),
|
|
300
|
+
timeout=timeout,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
await process.wait()
|
|
304
|
+
return (
|
|
305
|
+
process.returncode == 0,
|
|
306
|
+
"安装完成" if process.returncode == 0 else "安装失败",
|
|
307
|
+
)
|
|
308
|
+
except asyncio.TimeoutError:
|
|
309
|
+
return False, "安装超时"
|
|
310
|
+
finally:
|
|
311
|
+
if process and process.returncode is None:
|
|
312
|
+
process.kill()
|
|
313
|
+
await process.wait()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def install_browser(
|
|
317
|
+
browser_type: str = "chromium", timeout: int = 300, retries: int = 2
|
|
318
|
+
) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
安装用于 HTML 渲染的浏览器。
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
browser_type (str): 要安装的浏览器类型 ("chromium"、"firefox" 或 "webkit")。
|
|
324
|
+
timeout (int): 安装过程中等待的最大秒数。
|
|
325
|
+
retries (int): 安装失败时的最大重试次数。
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
bool: 如果安装成功返回 True,否则返回 False
|
|
329
|
+
"""
|
|
330
|
+
mirrors = [
|
|
331
|
+
MirrorSource("淘宝", "https://npmmirror.com/mirrors/playwright/", 1),
|
|
332
|
+
]
|
|
333
|
+
async with download_host_context(mirrors):
|
|
334
|
+
install_cmd = ["playwright", "install", "--with-deps", browser_type]
|
|
335
|
+
|
|
336
|
+
for attempt in range(1, retries + 1):
|
|
337
|
+
try:
|
|
338
|
+
logger.info(
|
|
339
|
+
f"正在安装 {browser_type}(第 {attempt}/{retries} 次尝试)..."
|
|
340
|
+
)
|
|
341
|
+
success, message = await execute_install_command(install_cmd, timeout)
|
|
342
|
+
|
|
343
|
+
if success:
|
|
344
|
+
logger.success(f"{browser_type} 安装成功")
|
|
345
|
+
return True
|
|
346
|
+
else:
|
|
347
|
+
logger.error(f"安装失败:{message}")
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"安装过程中出现异常:{str(e)}")
|
|
351
|
+
|
|
352
|
+
if attempt < retries:
|
|
353
|
+
await asyncio.sleep(2)
|
|
354
|
+
|
|
355
|
+
logger.error(f"在 {retries} 次尝试后安装失败")
|
|
356
|
+
return False
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
|
+
from nonebot import get_driver, get_plugin_config
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class Config(BaseModel):
|
|
@@ -9,4 +10,8 @@ class Config(BaseModel):
|
|
|
9
10
|
htmlrender_proxy_host: Optional[str] = Field(default=None)
|
|
10
11
|
htmlrender_browser_channel: Optional[str] = Field(default=None)
|
|
11
12
|
htmlrender_browser_executable_path: Optional[str] = Field(default=None)
|
|
12
|
-
htmlrender_connect_over_cdp: Optional[str] = Field(default=
|
|
13
|
+
htmlrender_connect_over_cdp: Optional[str] = Field(default=None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
global_config = get_driver().config
|
|
17
|
+
plugin_config = get_plugin_config(Config)
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nonebot-plugin-htmlrender"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "通过浏览器渲染图片"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "kexue", email = "x@kexue-cloud.cn" }]
|
|
7
7
|
requires-python = ">=3.9,<4.0"
|
|
8
8
|
license = { file = "LICENSE" }
|
|
9
9
|
dependencies = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
"aiofiles>=0.8.0",
|
|
11
|
+
"jinja2>=3.0.3",
|
|
12
|
+
"markdown>=3.3.6",
|
|
13
|
+
"nonebot2>=2.2.0",
|
|
14
|
+
"playwright>=1.48.0",
|
|
15
|
+
"pygments>=2.10.0",
|
|
16
|
+
"pymdown-extensions>=9.1",
|
|
17
|
+
"python-markdown-math>=0.8",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.urls]
|
|
@@ -28,13 +28,28 @@ asyncio_mode = "auto"
|
|
|
28
28
|
|
|
29
29
|
[dependency-groups]
|
|
30
30
|
dev = [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
"nonebot-adapter-onebot>=2.4.6",
|
|
32
|
+
"nonebot2[fastapi]>=2.3.3",
|
|
33
|
+
"nonebug>=0.4.2",
|
|
34
|
+
"pillow>=11.0.0",
|
|
35
|
+
"pytest-asyncio>=0.24.0",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
+
[tool.pyright]
|
|
39
|
+
pythonVersion = "3.9"
|
|
40
|
+
pythonPlatform = "All"
|
|
41
|
+
defineConstant = { PYDANTIC_V2 = true }
|
|
42
|
+
executionEnvironments = [
|
|
43
|
+
{ root = "./tests", extraPaths = [
|
|
44
|
+
"./",
|
|
45
|
+
] },
|
|
46
|
+
{ root = "./" },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
typeCheckingMode = "standard"
|
|
50
|
+
reportShadowedImports = false
|
|
51
|
+
disableBytesTypePromotions = true
|
|
52
|
+
|
|
38
53
|
[build-system]
|
|
39
54
|
requires = ["hatchling"]
|
|
40
55
|
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import nonebot
|
|
2
|
+
import pytest
|
|
3
|
+
from pytest_asyncio import is_async_test
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def pytest_collection_modifyitems(items: list[pytest.Item]):
|
|
7
|
+
pytest_asyncio_tests = (item for item in items if is_async_test(item))
|
|
8
|
+
session_scope_marker = pytest.mark.asyncio(loop_scope="session")
|
|
9
|
+
for async_test in pytest_asyncio_tests:
|
|
10
|
+
async_test.add_marker(session_scope_marker, append=False)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
14
|
+
async def after_nonebot_init(after_nonebot_init: None):
|
|
15
|
+
nonebot.require("nonebot_plugin_htmlrender")
|
|
@@ -612,7 +612,7 @@ wheels = [
|
|
|
612
612
|
|
|
613
613
|
[[package]]
|
|
614
614
|
name = "nonebot-plugin-htmlrender"
|
|
615
|
-
version = "0.
|
|
615
|
+
version = "0.5.1"
|
|
616
616
|
source = { editable = "." }
|
|
617
617
|
dependencies = [
|
|
618
618
|
{ name = "aiofiles" },
|
|
@@ -650,7 +650,7 @@ requires-dist = [
|
|
|
650
650
|
dev = [
|
|
651
651
|
{ name = "nonebot-adapter-onebot", specifier = ">=2.4.6" },
|
|
652
652
|
{ name = "nonebot2", extras = ["fastapi"], specifier = ">=2.3.3" },
|
|
653
|
-
{ name = "nonebug", specifier = ">=0.4.
|
|
653
|
+
{ name = "nonebug", specifier = ">=0.4.2" },
|
|
654
654
|
{ name = "pillow", specifier = ">=11.0.0" },
|
|
655
655
|
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
|
|
656
656
|
]
|
|
@@ -681,7 +681,7 @@ fastapi = [
|
|
|
681
681
|
|
|
682
682
|
[[package]]
|
|
683
683
|
name = "nonebug"
|
|
684
|
-
version = "0.4.
|
|
684
|
+
version = "0.4.2"
|
|
685
685
|
source = { registry = "https://pypi.org/simple" }
|
|
686
686
|
dependencies = [
|
|
687
687
|
{ name = "asgiref" },
|
|
@@ -690,9 +690,9 @@ dependencies = [
|
|
|
690
690
|
{ name = "pytest" },
|
|
691
691
|
{ name = "typing-extensions" },
|
|
692
692
|
]
|
|
693
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
693
|
+
sdist = { url = "https://files.pythonhosted.org/packages/29/56/01eb465dc135787e463423bf4762b883159a639d02c3a577ab4efc8f9d56/nonebug-0.4.2.tar.gz", hash = "sha256:002dac00fed1fbff97574f3dffab2237fc41473b3bf3e73d2730ae34895bc137", size = 12118 }
|
|
694
694
|
wheels = [
|
|
695
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
695
|
+
{ url = "https://files.pythonhosted.org/packages/a0/4a/7592e899cd5074a9b70df007afe9b6f8bbaa02b8e9c1ce7f5c5498634cb7/nonebug-0.4.2-py3-none-any.whl", hash = "sha256:83d9229ec899819e9f5786eabc0c27b9dd0cd5d6e2e7438f59923ab822d5c555", size = 15030 },
|
|
696
696
|
]
|
|
697
697
|
|
|
698
698
|
[[package]]
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
@Author : yanyongyu
|
|
5
|
-
@Date : 2021-03-12 13:42:43
|
|
6
|
-
@LastEditors : yanyongyu
|
|
7
|
-
@LastEditTime : 2021-11-01 14:05:41
|
|
8
|
-
@Description : None
|
|
9
|
-
@GitHub : https://github.com/yanyongyu
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
__author__ = "yanyongyu"
|
|
13
|
-
|
|
14
|
-
from contextlib import asynccontextmanager
|
|
15
|
-
from typing import AsyncIterator, Optional
|
|
16
|
-
|
|
17
|
-
from nonebot import get_plugin_config
|
|
18
|
-
from nonebot.log import logger
|
|
19
|
-
from playwright.async_api import Browser, Error, Page, Playwright, async_playwright
|
|
20
|
-
|
|
21
|
-
from .config import Config
|
|
22
|
-
|
|
23
|
-
config = get_plugin_config(Config)
|
|
24
|
-
|
|
25
|
-
_browser: Optional[Browser] = None
|
|
26
|
-
_playwright: Optional[Playwright] = None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
async def init(**kwargs) -> Browser:
|
|
30
|
-
global _browser
|
|
31
|
-
global _playwright
|
|
32
|
-
_playwright = await async_playwright().start()
|
|
33
|
-
try:
|
|
34
|
-
_browser = await launch_browser(**kwargs)
|
|
35
|
-
except Error:
|
|
36
|
-
await install_browser()
|
|
37
|
-
_browser = await launch_browser(**kwargs)
|
|
38
|
-
return _browser
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
async def launch_browser(**kwargs) -> Browser:
|
|
42
|
-
assert _playwright is not None, "Playwright 未安装"
|
|
43
|
-
|
|
44
|
-
if config.htmlrender_browser_channel:
|
|
45
|
-
kwargs["channel"] = config.htmlrender_browser_channel
|
|
46
|
-
|
|
47
|
-
if config.htmlrender_proxy_host:
|
|
48
|
-
kwargs["proxy"] = {
|
|
49
|
-
"server": config.htmlrender_proxy_host,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if config.htmlrender_browser_executable_path:
|
|
53
|
-
kwargs["executable_path"] = config.htmlrender_browser_executable_path
|
|
54
|
-
|
|
55
|
-
if config.htmlrender_browser:
|
|
56
|
-
if config.htmlrender_browser.lower() == "firefox":
|
|
57
|
-
logger.info("使用 Firefox 启动")
|
|
58
|
-
return await _playwright.firefox.launch(**kwargs)
|
|
59
|
-
elif config.htmlrender_browser.lower() == "webkit":
|
|
60
|
-
logger.info("使用 Webkit 启动")
|
|
61
|
-
return await _playwright.webkit.launch(**kwargs)
|
|
62
|
-
else:
|
|
63
|
-
logger.warning("浏览器参数错误,无 {} 浏览器核心可用,使用 Chromium 启动")
|
|
64
|
-
|
|
65
|
-
if config.htmlrender_connect_over_cdp:
|
|
66
|
-
logger.info("使用 chromium connect_over_cdp 启动")
|
|
67
|
-
return await _playwright.chromium.connect_over_cdp(**kwargs)
|
|
68
|
-
|
|
69
|
-
# 默认使用 chromium
|
|
70
|
-
logger.info("使用 Chromium 启动")
|
|
71
|
-
return await _playwright.chromium.launch(**kwargs)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async def get_browser(**kwargs) -> Browser:
|
|
75
|
-
return _browser if _browser and _browser.is_connected() else await init(**kwargs)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@asynccontextmanager
|
|
79
|
-
async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
|
|
80
|
-
browser = await get_browser()
|
|
81
|
-
page = await browser.new_page(device_scale_factor=device_scale_factor, **kwargs)
|
|
82
|
-
try:
|
|
83
|
-
yield page
|
|
84
|
-
finally:
|
|
85
|
-
await page.close()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
async def shutdown_browser():
|
|
89
|
-
global _browser
|
|
90
|
-
global _playwright
|
|
91
|
-
if _browser:
|
|
92
|
-
if _browser.is_connected():
|
|
93
|
-
await _browser.close()
|
|
94
|
-
_browser = None
|
|
95
|
-
if _playwright:
|
|
96
|
-
# await _playwright.stop()
|
|
97
|
-
_playwright = None
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
async def install_browser():
|
|
101
|
-
import os
|
|
102
|
-
import sys
|
|
103
|
-
|
|
104
|
-
from playwright.__main__ import main
|
|
105
|
-
|
|
106
|
-
if host := config.htmlrender_download_host:
|
|
107
|
-
logger.info("使用配置源进行下载")
|
|
108
|
-
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = host
|
|
109
|
-
else:
|
|
110
|
-
logger.info("使用镜像源进行下载")
|
|
111
|
-
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = (
|
|
112
|
-
"https://npmmirror.com/mirrors/playwright/"
|
|
113
|
-
)
|
|
114
|
-
success = False
|
|
115
|
-
|
|
116
|
-
if config.htmlrender_browser == "firefox":
|
|
117
|
-
logger.info("正在安装 firefox")
|
|
118
|
-
sys.argv = ["", "install", "firefox"]
|
|
119
|
-
else:
|
|
120
|
-
# 默认使用 chromium
|
|
121
|
-
logger.info("正在安装 chromium")
|
|
122
|
-
sys.argv = ["", "install", "chromium"]
|
|
123
|
-
try:
|
|
124
|
-
logger.info("正在安装依赖")
|
|
125
|
-
os.system("playwright install-deps") # noqa: ASYNC102, S605, S607
|
|
126
|
-
main()
|
|
127
|
-
except SystemExit as e:
|
|
128
|
-
if e.code == 0:
|
|
129
|
-
success = True
|
|
130
|
-
if not success:
|
|
131
|
-
logger.error("浏览器更新失败, 请检查网络连通性")
|
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.github/workflows/codecov.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/markdown.css
RENAMED
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/mystyle.css
RENAMED
|
File without changes
|
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/text.html
RENAMED
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/test_htmlrender.py
RENAMED
|
File without changes
|
{nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/test_template_filter.png
RENAMED
|
File without changes
|
|
File without changes
|