nonebot-plugin-htmlrender 0.6.6__tar.gz → 0.6.8__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.6.6 → nonebot_plugin_htmlrender-0.6.8}/.pre-commit-config.yaml +5 -5
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/PKG-INFO +2 -1
- nonebot_plugin_htmlrender-0.6.8/nonebot_plugin_htmlrender/__init__.py +64 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/browser.py +59 -34
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/config.py +22 -1
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/data_source.py +8 -7
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/install.py +32 -23
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/process.py +39 -11
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/utils.py +62 -1
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/pyproject.toml +2 -1
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/test_browser.py +77 -23
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/test_htmlrender.py +3 -3
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/test_process.py +17 -30
- nonebot_plugin_htmlrender-0.6.8/uv.lock +1952 -0
- nonebot_plugin_htmlrender-0.6.6/nonebot_plugin_htmlrender/__init__.py +0 -55
- nonebot_plugin_htmlrender-0.6.6/uv.lock +0 -1828
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/.dockerignore +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/codecov.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/docker_image.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/publish.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/.gitignore +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/.python-version +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/LICENSE +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/README.md +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/docker-compose.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/dockerfile +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/docs/example.md +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/docs/html2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/docs/md2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/docs/template2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/docs/text2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/entrypoint.sh +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/docker-compose.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/entrypoint.sh +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/html2pic.html +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/example/pyproject.toml +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/consts.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/signal.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/text.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/conftest.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/resources/test_template_filter.png +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/test_deprecated_decorator.py +0 -0
- {nonebot_plugin_htmlrender-0.6.6 → nonebot_plugin_htmlrender-0.6.8}/tests/test_install.py +0 -0
|
@@ -7,10 +7,10 @@ ci:
|
|
|
7
7
|
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
|
8
8
|
repos:
|
|
9
9
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
10
|
-
rev: v0.
|
|
10
|
+
rev: v0.12.12
|
|
11
11
|
hooks:
|
|
12
|
-
- id: ruff
|
|
13
|
-
args: [--fix]
|
|
14
|
-
stages: [pre-commit]
|
|
15
12
|
- id: ruff-format
|
|
16
|
-
|
|
13
|
+
- id: ruff-check
|
|
14
|
+
args: [ --fix, --exit-non-zero-on-fix ]
|
|
15
|
+
types_or: [ python, pyi ]
|
|
16
|
+
require_serial: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-htmlrender
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.8
|
|
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
|
|
@@ -31,6 +31,7 @@ Requires-Python: <4.0,>=3.9
|
|
|
31
31
|
Requires-Dist: aiofiles>=0.8.0
|
|
32
32
|
Requires-Dist: jinja2>=3.0.3
|
|
33
33
|
Requires-Dist: markdown>=3.3.6
|
|
34
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.4
|
|
34
35
|
Requires-Dist: nonebot2>=2.4.2
|
|
35
36
|
Requires-Dist: playwright>=1.48.0
|
|
36
37
|
Requires-Dist: pygments>=2.10.0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import nonebot
|
|
2
|
+
from nonebot import require
|
|
3
|
+
|
|
4
|
+
require("nonebot_plugin_localstore")
|
|
5
|
+
from nonebot.log import logger
|
|
6
|
+
from nonebot.plugin import PluginMetadata
|
|
7
|
+
|
|
8
|
+
from nonebot_plugin_htmlrender.browser import (
|
|
9
|
+
get_new_page,
|
|
10
|
+
shutdown_htmlrender,
|
|
11
|
+
startup_htmlrender,
|
|
12
|
+
)
|
|
13
|
+
from nonebot_plugin_htmlrender.config import Config, plugin_config
|
|
14
|
+
from nonebot_plugin_htmlrender.data_source import (
|
|
15
|
+
capture_element,
|
|
16
|
+
html_to_pic,
|
|
17
|
+
md_to_pic,
|
|
18
|
+
template_to_html,
|
|
19
|
+
template_to_pic,
|
|
20
|
+
text_to_pic,
|
|
21
|
+
)
|
|
22
|
+
from nonebot_plugin_htmlrender.utils import _clear_playwright_env_vars
|
|
23
|
+
|
|
24
|
+
__plugin_meta__ = PluginMetadata(
|
|
25
|
+
name="nonebot-plugin-htmlrender",
|
|
26
|
+
description="通过浏览器渲染图片",
|
|
27
|
+
usage="",
|
|
28
|
+
type="library",
|
|
29
|
+
config=Config,
|
|
30
|
+
homepage="https://github.com/kexue-z/nonebot-plugin-htmlrender",
|
|
31
|
+
extra={},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
driver = nonebot.get_driver()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@driver.on_startup
|
|
38
|
+
async def init(**kwargs):
|
|
39
|
+
logger.info("HTMLRender Starting...")
|
|
40
|
+
await startup_htmlrender(**kwargs)
|
|
41
|
+
logger.opt(colors=True).info(
|
|
42
|
+
f"HTMLRender Started with <cyan>{plugin_config.htmlrender_browser}</cyan>."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@driver.on_shutdown
|
|
47
|
+
async def shutdown():
|
|
48
|
+
logger.info("HTMLRender Shutting down...")
|
|
49
|
+
await shutdown_htmlrender()
|
|
50
|
+
_clear_playwright_env_vars()
|
|
51
|
+
logger.info("HTMLRender Shut down.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"capture_element",
|
|
56
|
+
"get_new_page",
|
|
57
|
+
"html_to_pic",
|
|
58
|
+
"md_to_pic",
|
|
59
|
+
"shutdown_htmlrender",
|
|
60
|
+
"startup_htmlrender",
|
|
61
|
+
"template_to_html",
|
|
62
|
+
"template_to_pic",
|
|
63
|
+
"text_to_pic",
|
|
64
|
+
]
|
|
@@ -13,7 +13,13 @@ from playwright.async_api import (
|
|
|
13
13
|
|
|
14
14
|
from nonebot_plugin_htmlrender.config import plugin_config
|
|
15
15
|
from nonebot_plugin_htmlrender.install import install_browser
|
|
16
|
-
from nonebot_plugin_htmlrender.utils import
|
|
16
|
+
from nonebot_plugin_htmlrender.utils import (
|
|
17
|
+
_prepare_playwright_env_vars,
|
|
18
|
+
clean_playwright_cache,
|
|
19
|
+
proxy_settings,
|
|
20
|
+
suppress_and_log,
|
|
21
|
+
with_lock,
|
|
22
|
+
)
|
|
17
23
|
|
|
18
24
|
_browser: Optional[Browser] = None
|
|
19
25
|
_playwright: Optional[Playwright] = None
|
|
@@ -40,22 +46,6 @@ async def _launch(browser_type: str, **kwargs) -> Browser:
|
|
|
40
46
|
return await _browser_cls.launch(**kwargs)
|
|
41
47
|
|
|
42
48
|
|
|
43
|
-
async def init_browser(**kwargs) -> Browser:
|
|
44
|
-
"""
|
|
45
|
-
初始化浏览器实例。
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
**kwargs: 传递给`playwright.launch`的关键字参数。
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Browser: 浏览器实例。
|
|
52
|
-
|
|
53
|
-
Raises:
|
|
54
|
-
RuntimeError: 如果浏览器无法启动或安装失败。
|
|
55
|
-
"""
|
|
56
|
-
return await start_browser(**kwargs)
|
|
57
|
-
|
|
58
|
-
|
|
59
49
|
@asynccontextmanager
|
|
60
50
|
async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
|
|
61
51
|
"""
|
|
@@ -88,7 +78,7 @@ async def get_browser(**kwargs) -> Browser:
|
|
|
88
78
|
if _browser and _browser.is_connected():
|
|
89
79
|
return _browser
|
|
90
80
|
|
|
91
|
-
return await
|
|
81
|
+
return await startup_htmlrender(**kwargs)
|
|
92
82
|
|
|
93
83
|
|
|
94
84
|
async def _connect_via_cdp(**kwargs) -> Browser:
|
|
@@ -106,12 +96,12 @@ async def _connect_via_cdp(**kwargs) -> Browser:
|
|
|
106
96
|
"""
|
|
107
97
|
kwargs["endpoint_url"] = plugin_config.htmlrender_connect_over_cdp
|
|
108
98
|
logger.info(
|
|
109
|
-
f"
|
|
99
|
+
f"Connecting to Chromium via CDP ({plugin_config.htmlrender_connect_over_cdp})"
|
|
110
100
|
)
|
|
111
101
|
if _playwright is not None:
|
|
112
102
|
return await _playwright.chromium.connect_over_cdp(**kwargs)
|
|
113
103
|
else:
|
|
114
|
-
raise RuntimeError("Playwright
|
|
104
|
+
raise RuntimeError("Playwright is not initialized")
|
|
115
105
|
|
|
116
106
|
|
|
117
107
|
async def _connect(browser_type: str, **kwargs) -> Browser:
|
|
@@ -131,7 +121,8 @@ async def _connect(browser_type: str, **kwargs) -> Browser:
|
|
|
131
121
|
_browser_cls: BrowserType = getattr(_playwright, browser_type)
|
|
132
122
|
kwargs["ws_endpoint"] = plugin_config.htmlrender_connect
|
|
133
123
|
logger.info(
|
|
134
|
-
f"
|
|
124
|
+
f"Connecting to {browser_type.capitalize()} via "
|
|
125
|
+
f"WebSocket endpoint: {plugin_config.htmlrender_connect}"
|
|
135
126
|
)
|
|
136
127
|
if _playwright is not None:
|
|
137
128
|
return await _browser_cls.connect(**kwargs)
|
|
@@ -140,7 +131,7 @@ async def _connect(browser_type: str, **kwargs) -> Browser:
|
|
|
140
131
|
|
|
141
132
|
|
|
142
133
|
@with_lock
|
|
143
|
-
async def
|
|
134
|
+
async def startup_htmlrender(**kwargs) -> Browser:
|
|
144
135
|
"""
|
|
145
136
|
启动 Playwright 浏览器实例。
|
|
146
137
|
|
|
@@ -152,8 +143,12 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
152
143
|
"""
|
|
153
144
|
global _browser, _playwright
|
|
154
145
|
|
|
155
|
-
await
|
|
146
|
+
await shutdown_htmlrender()
|
|
147
|
+
clean_playwright_cache()
|
|
148
|
+
_prepare_playwright_env_vars()
|
|
149
|
+
|
|
156
150
|
_playwright = await async_playwright().start()
|
|
151
|
+
logger.debug("Playwright started")
|
|
157
152
|
|
|
158
153
|
if (
|
|
159
154
|
plugin_config.htmlrender_browser == "chromium"
|
|
@@ -169,37 +164,67 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
169
164
|
if plugin_config.htmlrender_proxy_host:
|
|
170
165
|
kwargs["proxy"] = proxy_settings(plugin_config.htmlrender_proxy_host)
|
|
171
166
|
|
|
167
|
+
if plugin_config.htmlrender_browser_args:
|
|
168
|
+
kwargs["args"] = plugin_config.htmlrender_browser_args.split()
|
|
169
|
+
|
|
172
170
|
if plugin_config.htmlrender_browser_executable_path:
|
|
173
171
|
kwargs["executable_path"] = plugin_config.htmlrender_browser_executable_path
|
|
172
|
+
try:
|
|
173
|
+
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
f"Failed to launch browser with executable path"
|
|
177
|
+
f" '{plugin_config.htmlrender_browser_executable_path}': {e}"
|
|
178
|
+
) from e
|
|
174
179
|
else:
|
|
175
180
|
try:
|
|
176
|
-
await check_playwright_env(**kwargs)
|
|
181
|
+
_browser = await check_playwright_env(**kwargs)
|
|
177
182
|
except RuntimeError:
|
|
178
183
|
await install_browser()
|
|
179
|
-
await check_playwright_env(**kwargs)
|
|
180
|
-
|
|
181
|
-
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
184
|
+
_browser = await check_playwright_env(**kwargs)
|
|
182
185
|
|
|
183
186
|
return _browser
|
|
184
187
|
|
|
185
188
|
|
|
186
|
-
async def
|
|
189
|
+
async def shutdown_htmlrender() -> None:
|
|
187
190
|
"""关闭浏览器和 Playwright 实例。"""
|
|
188
191
|
if _browser:
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
if not _browser.is_connected():
|
|
193
|
+
logger.info("Browser was already disconnected, skipping close.")
|
|
194
|
+
else:
|
|
195
|
+
with suppress_and_log():
|
|
196
|
+
logger.debug("Disconnecting browser...")
|
|
197
|
+
await _browser.close()
|
|
198
|
+
logger.info("Disconnected browser.")
|
|
191
199
|
if _playwright:
|
|
192
200
|
with suppress_and_log():
|
|
201
|
+
logger.debug("Stopping Playwright...")
|
|
193
202
|
await _playwright.stop()
|
|
203
|
+
logger.info("Playwright stopped.")
|
|
204
|
+
|
|
194
205
|
|
|
206
|
+
async def check_playwright_env(**kwargs) -> Browser:
|
|
207
|
+
"""
|
|
208
|
+
检查Playwright环境,复用_launch方法避免逻辑重复。
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
**kwargs: 传递给`playwright.launch`的关键字参数。
|
|
195
212
|
|
|
196
|
-
|
|
197
|
-
|
|
213
|
+
Raises:
|
|
214
|
+
RuntimeError: 如果Playwright环境设置不正确。
|
|
215
|
+
"""
|
|
198
216
|
logger.info("Checking Playwright environment...")
|
|
217
|
+
global _browser, _playwright
|
|
218
|
+
|
|
199
219
|
try:
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
_playwright = await async_playwright().start()
|
|
221
|
+
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
222
|
+
logger.success("Playwright environment is set up correctly.")
|
|
223
|
+
return _browser
|
|
224
|
+
|
|
202
225
|
except Exception as e:
|
|
226
|
+
await shutdown_htmlrender()
|
|
227
|
+
|
|
203
228
|
raise RuntimeError(
|
|
204
229
|
"Playwright environment is not set up correctly. "
|
|
205
230
|
"Refer to https://playwright.dev/python/docs/intro#system-requirements"
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
from pathlib import Path
|
|
1
2
|
from typing import Any, Optional
|
|
2
3
|
|
|
3
4
|
from nonebot import get_driver, get_plugin_config
|
|
4
5
|
from nonebot.compat import model_validator
|
|
6
|
+
import nonebot_plugin_localstore as store
|
|
5
7
|
from pydantic import BaseModel, Field
|
|
6
8
|
|
|
7
9
|
from nonebot_plugin_htmlrender.consts import BROWSER_CHANNEL_TYPES, BROWSER_ENGINE_TYPES
|
|
8
10
|
|
|
11
|
+
plugin_cache_dir: Path = store.get_plugin_cache_dir()
|
|
12
|
+
plugin_config_dir: Path = store.get_plugin_config_dir()
|
|
13
|
+
plugin_data_dir: Path = store.get_plugin_data_dir()
|
|
14
|
+
|
|
9
15
|
|
|
10
16
|
class Config(BaseModel):
|
|
11
17
|
"""插件配置类。"""
|
|
@@ -14,6 +20,18 @@ class Config(BaseModel):
|
|
|
14
20
|
default="chromium",
|
|
15
21
|
description="Playwright浏览器引擎类型,默认值为 'chromium'。",
|
|
16
22
|
)
|
|
23
|
+
htmlrender_storage_path: Path = Field(
|
|
24
|
+
default=plugin_data_dir,
|
|
25
|
+
description="存储路径,不填则使用 `nonebot-plugin-localstore` 管理",
|
|
26
|
+
)
|
|
27
|
+
htmlrender_cache_path: Path = Field(
|
|
28
|
+
default=plugin_cache_dir,
|
|
29
|
+
description="缓存路径,不填则使用 `nonebot-plugin-localstore` 管理",
|
|
30
|
+
)
|
|
31
|
+
htmlrender_config_path: Path = Field(
|
|
32
|
+
default=plugin_config_dir,
|
|
33
|
+
description="配置路径,不填则使用 `nonebot-plugin-localstore` 管理",
|
|
34
|
+
)
|
|
17
35
|
htmlrender_download_host: Optional[str] = Field(
|
|
18
36
|
default=None, description="下载Playwright浏览器时的主机地址。"
|
|
19
37
|
)
|
|
@@ -29,7 +47,7 @@ class Config(BaseModel):
|
|
|
29
47
|
htmlrender_browser_channel: Optional[str] = Field(
|
|
30
48
|
default=None, description="Playwright浏览器通道类型。"
|
|
31
49
|
)
|
|
32
|
-
htmlrender_browser_executable_path: Optional[
|
|
50
|
+
htmlrender_browser_executable_path: Optional[Path] = Field(
|
|
33
51
|
default=None, description="Playwright浏览器可执行文件的路径。"
|
|
34
52
|
)
|
|
35
53
|
htmlrender_connect_over_cdp: Optional[str] = Field(
|
|
@@ -38,6 +56,9 @@ class Config(BaseModel):
|
|
|
38
56
|
htmlrender_connect: Optional[str] = Field(
|
|
39
57
|
default=None, description="通过Playwright协议连接Playwright浏览器的端点地址。"
|
|
40
58
|
)
|
|
59
|
+
htmlrender_browser_args: Optional[str] = Field(
|
|
60
|
+
default=None, description="Playwright 浏览器启动参数。"
|
|
61
|
+
)
|
|
41
62
|
|
|
42
63
|
@model_validator(mode="after")
|
|
43
64
|
@classmethod
|
|
@@ -87,7 +87,7 @@ async def md_to_pic(
|
|
|
87
87
|
if md_path:
|
|
88
88
|
md = await read_file(md_path)
|
|
89
89
|
else:
|
|
90
|
-
raise Exception("
|
|
90
|
+
raise Exception("md or md_path must be provided")
|
|
91
91
|
logger.debug(md)
|
|
92
92
|
md = markdown.markdown(
|
|
93
93
|
md,
|
|
@@ -141,7 +141,7 @@ async def md_to_pic(
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
async def read_file(path: str) -> str:
|
|
144
|
-
async with aiofiles.open(path) as f:
|
|
144
|
+
async with aiofiles.open(path, encoding="UTF8") as f:
|
|
145
145
|
return await f.read()
|
|
146
146
|
|
|
147
147
|
|
|
@@ -174,7 +174,7 @@ async def template_to_html(
|
|
|
174
174
|
if filters:
|
|
175
175
|
for filter_name, filter_func in filters.items():
|
|
176
176
|
template_env.filters[filter_name] = filter_func
|
|
177
|
-
logger.debug(f"
|
|
177
|
+
logger.debug(f"Custom filter loaded: {filter_name}")
|
|
178
178
|
|
|
179
179
|
template = template_env.get_template(template_name)
|
|
180
180
|
|
|
@@ -189,6 +189,7 @@ async def html_to_pic(
|
|
|
189
189
|
quality: Union[int, None] = None,
|
|
190
190
|
device_scale_factor: float = 2,
|
|
191
191
|
screenshot_timeout: Optional[float] = 30_000,
|
|
192
|
+
full_page: Optional[bool] = True,
|
|
192
193
|
**kwargs,
|
|
193
194
|
) -> bytes:
|
|
194
195
|
"""html转图片
|
|
@@ -208,14 +209,14 @@ async def html_to_pic(
|
|
|
208
209
|
"""
|
|
209
210
|
# logger.debug(f"html:\n{html}")
|
|
210
211
|
if "file:" not in template_path:
|
|
211
|
-
raise Exception("template_path
|
|
212
|
+
raise Exception("template_path should be file:///path/to/template")
|
|
212
213
|
async with get_new_page(device_scale_factor, **kwargs) as page:
|
|
213
|
-
page.on("console", lambda msg: logger.debug(f"
|
|
214
|
+
page.on("console", lambda msg: logger.debug(f"[Browser Console]: {msg.text}"))
|
|
214
215
|
await page.goto(template_path)
|
|
215
216
|
await page.set_content(html, wait_until="networkidle")
|
|
216
217
|
await page.wait_for_timeout(wait)
|
|
217
218
|
return await page.screenshot(
|
|
218
|
-
full_page=
|
|
219
|
+
full_page=full_page,
|
|
219
220
|
type=type,
|
|
220
221
|
quality=quality,
|
|
221
222
|
timeout=screenshot_timeout,
|
|
@@ -265,7 +266,7 @@ async def template_to_pic(
|
|
|
265
266
|
if filters:
|
|
266
267
|
for filter_name, filter_func in filters.items():
|
|
267
268
|
template_env.filters[filter_name] = filter_func
|
|
268
|
-
logger.debug(f"
|
|
269
|
+
logger.debug(f"Custom filter loaded: {filter_name}")
|
|
269
270
|
|
|
270
271
|
template = template_env.get_template(template_name)
|
|
271
272
|
|
|
@@ -86,7 +86,10 @@ async def download_context() -> AsyncIterator[None]:
|
|
|
86
86
|
try:
|
|
87
87
|
best_mirror = await check_mirror_connectivity()
|
|
88
88
|
if best_mirror is not None:
|
|
89
|
-
logger.
|
|
89
|
+
logger.opt(colors=True).info(
|
|
90
|
+
f"Using Mirror source: <cyan>{best_mirror.name}</cyan> "
|
|
91
|
+
f"{best_mirror.url}"
|
|
92
|
+
)
|
|
90
93
|
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = best_mirror.url
|
|
91
94
|
else:
|
|
92
95
|
logger.info("Mirror source not available, using default")
|
|
@@ -125,30 +128,25 @@ async def read_stream(
|
|
|
125
128
|
if stream is None:
|
|
126
129
|
return ""
|
|
127
130
|
|
|
128
|
-
last_progress = "" # 上一次显示的进度
|
|
129
131
|
output = [] # 存储读取到的文本内容
|
|
130
132
|
|
|
131
133
|
while True:
|
|
132
134
|
try:
|
|
133
|
-
|
|
134
|
-
if not
|
|
135
|
+
line_data = await stream.readline()
|
|
136
|
+
if not line_data:
|
|
135
137
|
break
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
continue
|
|
139
|
+
line = line_data.decode().strip()
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
if callback:
|
|
142
|
+
try:
|
|
143
|
+
await callback(line)
|
|
144
|
+
except UnicodeEncodeError:
|
|
145
|
+
safe_text = "".join(c if ord(c) < 128 else "?" for c in line)
|
|
146
|
+
await callback(safe_text)
|
|
142
147
|
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
if callback:
|
|
146
|
-
await callback(f"Progress: {text}")
|
|
147
|
-
last_progress = text
|
|
148
|
-
else:
|
|
149
|
-
if callback:
|
|
150
|
-
await callback(text)
|
|
151
|
-
output.append(text)
|
|
148
|
+
if line:
|
|
149
|
+
output.append(line)
|
|
152
150
|
|
|
153
151
|
except asyncio.IncompleteReadError:
|
|
154
152
|
break
|
|
@@ -171,6 +169,7 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
171
169
|
try:
|
|
172
170
|
logger.debug("Starting playwright install process...")
|
|
173
171
|
install_signal_handler()
|
|
172
|
+
|
|
174
173
|
process = await create_process(
|
|
175
174
|
"playwright",
|
|
176
175
|
"install",
|
|
@@ -179,13 +178,21 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
179
178
|
stdout=asyncio.subprocess.PIPE,
|
|
180
179
|
stderr=asyncio.subprocess.PIPE,
|
|
181
180
|
)
|
|
182
|
-
logger.debug("Process created successfully")
|
|
183
181
|
|
|
184
182
|
async def stdout_callback(line: str) -> None:
|
|
185
|
-
|
|
183
|
+
line_stripped = line.strip()
|
|
184
|
+
if (
|
|
185
|
+
not line_stripped.startswith("Progress:")
|
|
186
|
+
and "|" not in line_stripped
|
|
187
|
+
and "%" not in line_stripped
|
|
188
|
+
and line_stripped
|
|
189
|
+
):
|
|
190
|
+
logger.info(line_stripped)
|
|
186
191
|
|
|
187
192
|
async def stderr_callback(line: str) -> None:
|
|
188
|
-
|
|
193
|
+
line_stripped = line.strip()
|
|
194
|
+
if line_stripped:
|
|
195
|
+
logger.warning(f"Install error: {line_stripped}")
|
|
189
196
|
|
|
190
197
|
stdout_task = asyncio.create_task(read_stream(process.stdout, stdout_callback))
|
|
191
198
|
stderr_task = asyncio.create_task(read_stream(process.stderr, stderr_callback))
|
|
@@ -195,18 +202,20 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
195
202
|
asyncio.gather(stdout_task, stderr_task), timeout=timeout
|
|
196
203
|
)
|
|
197
204
|
except asyncio.TimeoutError:
|
|
205
|
+
logger.error(f"Timed out ({timeout}秒)")
|
|
198
206
|
await terminate_process(process)
|
|
199
207
|
return False, f"Timed out ({timeout}s)"
|
|
200
208
|
|
|
201
209
|
await process.wait()
|
|
202
|
-
|
|
210
|
+
return_code = process.returncode
|
|
203
211
|
|
|
204
|
-
if
|
|
205
|
-
return False, f"Exited with code {
|
|
212
|
+
if return_code != 0:
|
|
213
|
+
return False, f"Exited with code {return_code}"
|
|
206
214
|
|
|
207
215
|
return True, "Installation completed"
|
|
208
216
|
|
|
209
217
|
except Exception as e:
|
|
218
|
+
logger.error(f"An error occurred during installation: {e!s}")
|
|
210
219
|
return False, f"An error occurred during installation: {e!s}"
|
|
211
220
|
|
|
212
221
|
|
|
@@ -96,14 +96,27 @@ async def create_process(
|
|
|
96
96
|
>>> async def func():
|
|
97
97
|
... return await create_process("ls", cwd="/")
|
|
98
98
|
"""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
if WINDOWS:
|
|
100
|
+
creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
101
|
+
return await asyncio.create_subprocess_exec(
|
|
102
|
+
*args,
|
|
103
|
+
cwd=cwd,
|
|
104
|
+
stdin=stdin,
|
|
105
|
+
stdout=stdout,
|
|
106
|
+
stderr=stderr,
|
|
107
|
+
creationflags=creation_flags,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
creation_flags = 0
|
|
111
|
+
return await asyncio.create_subprocess_exec(
|
|
112
|
+
*args,
|
|
113
|
+
cwd=cwd,
|
|
114
|
+
stdin=stdin,
|
|
115
|
+
stdout=stdout,
|
|
116
|
+
stderr=stderr,
|
|
117
|
+
creationflags=creation_flags,
|
|
118
|
+
start_new_session=True,
|
|
119
|
+
)
|
|
107
120
|
|
|
108
121
|
|
|
109
122
|
@ensure_process_terminated
|
|
@@ -160,6 +173,21 @@ async def terminate_process(process: asyncio.subprocess.Process) -> None:
|
|
|
160
173
|
if WINDOWS:
|
|
161
174
|
os.kill(process.pid, signal.CTRL_BREAK_EVENT)
|
|
162
175
|
else:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
try:
|
|
177
|
+
pgid = os.getpgid(process.pid)
|
|
178
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
179
|
+
except ProcessLookupError:
|
|
180
|
+
process.terminate()
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
184
|
+
except asyncio.TimeoutError:
|
|
185
|
+
if not WINDOWS:
|
|
186
|
+
try:
|
|
187
|
+
pgid = os.getpgid(process.pid)
|
|
188
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
189
|
+
except ProcessLookupError:
|
|
190
|
+
process.kill()
|
|
191
|
+
else:
|
|
192
|
+
process.kill()
|
|
193
|
+
await process.wait()
|
|
@@ -2,7 +2,11 @@ from asyncio import Lock
|
|
|
2
2
|
from collections.abc import Awaitable
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
from functools import wraps
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import platform
|
|
5
8
|
import re
|
|
9
|
+
import shutil
|
|
6
10
|
from typing import (
|
|
7
11
|
Any,
|
|
8
12
|
Callable,
|
|
@@ -103,7 +107,7 @@ def suppress_and_log():
|
|
|
103
107
|
yield
|
|
104
108
|
except Exception as e:
|
|
105
109
|
# 捕获异常并记录日志
|
|
106
|
-
logger.opt(exception=e).warning("
|
|
110
|
+
logger.opt(exception=e).warning("Error occurred while closing playwright.")
|
|
107
111
|
|
|
108
112
|
|
|
109
113
|
def proxy_settings(proxy_host: Optional[str]) -> Optional[dict]:
|
|
@@ -159,3 +163,60 @@ def with_lock(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
|
|
159
163
|
return await func(*args, **kwargs)
|
|
160
164
|
|
|
161
165
|
return wrapper
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _prepare_playwright_env_vars() -> None:
|
|
169
|
+
"""
|
|
170
|
+
准备启动浏览器所需的环境变量。
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dict[str, str]: 包含环境变量的字典
|
|
174
|
+
"""
|
|
175
|
+
if (
|
|
176
|
+
plugin_config.htmlrender_storage_path
|
|
177
|
+
and not plugin_config.htmlrender_browser_executable_path
|
|
178
|
+
):
|
|
179
|
+
storage_path = os.path.abspath(
|
|
180
|
+
os.path.expanduser(str(plugin_config.htmlrender_storage_path))
|
|
181
|
+
)
|
|
182
|
+
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = storage_path
|
|
183
|
+
|
|
184
|
+
logger.debug(f'Setting PLAYWRIGHT_BROWSERS_PATH="{storage_path}"')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _clear_playwright_env_vars() -> None:
|
|
188
|
+
if (
|
|
189
|
+
plugin_config.htmlrender_storage_path
|
|
190
|
+
and not plugin_config.htmlrender_browser_executable_path
|
|
191
|
+
) and "PLAYWRIGHT_BROWSERS_PATH" in os.environ:
|
|
192
|
+
playwright_path = os.environ.pop("PLAYWRIGHT_BROWSERS_PATH")
|
|
193
|
+
logger.debug(f'PLAYWRIGHT_BROWSERS_PATH="{playwright_path}" removed')
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def clean_playwright_cache() -> None:
|
|
197
|
+
system = platform.system()
|
|
198
|
+
home_dir = Path.home()
|
|
199
|
+
cache_path = None
|
|
200
|
+
|
|
201
|
+
if system == "Windows":
|
|
202
|
+
cache_path = home_dir / "AppData" / "Local" / "ms-playwright"
|
|
203
|
+
elif system == "Darwin":
|
|
204
|
+
cache_path = home_dir / "Library" / "Caches" / "ms-playwright"
|
|
205
|
+
elif system == "Linux":
|
|
206
|
+
cache_path = home_dir / ".cache" / "ms-playwright"
|
|
207
|
+
|
|
208
|
+
if cache_path and cache_path.exists():
|
|
209
|
+
try:
|
|
210
|
+
logger.warning(
|
|
211
|
+
"Since v0.7.0, nonebot-plugin-htmlrender has moved the Playwright"
|
|
212
|
+
"cache path. Executable files are now stored and managed by the "
|
|
213
|
+
"`nonebot-plugin-localstore` plugin under "
|
|
214
|
+
f"{plugin_config.htmlrender_storage_path}. "
|
|
215
|
+
"You can change this path via the config option "
|
|
216
|
+
"`htmlrender_storage_path`."
|
|
217
|
+
)
|
|
218
|
+
logger.info(f"Deleting Playwright directory at {cache_path}")
|
|
219
|
+
shutil.rmtree(str(cache_path))
|
|
220
|
+
logger.info("Playwright was cleaned successfully.")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Failed to delete Playwright: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nonebot-plugin-htmlrender"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.8"
|
|
4
4
|
description = "通过浏览器渲染图片"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "kexue", email = "x@kexue-cloud.cn" }]
|
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
|
10
10
|
"aiofiles>=0.8.0",
|
|
11
11
|
"jinja2>=3.0.3",
|
|
12
12
|
"markdown>=3.3.6",
|
|
13
|
+
"nonebot-plugin-localstore>=0.7.4",
|
|
13
14
|
"nonebot2>=2.4.2",
|
|
14
15
|
"playwright>=1.48.0",
|
|
15
16
|
"pygments>=2.10.0",
|