nonebot-plugin-htmlrender 0.6.7__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.7 → nonebot_plugin_htmlrender-0.6.8}/.pre-commit-config.yaml +5 -5
- {nonebot_plugin_htmlrender-0.6.7 → 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.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/browser.py +56 -34
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/config.py +19 -2
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/data_source.py +5 -5
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/install.py +32 -23
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/process.py +39 -11
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/utils.py +62 -1
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/pyproject.toml +2 -1
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_browser.py +77 -23
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_htmlrender.py +3 -3
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_process.py +17 -30
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/uv.lock +146 -120
- nonebot_plugin_htmlrender-0.6.7/nonebot_plugin_htmlrender/__init__.py +0 -55
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.dockerignore +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/codecov.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/docker_image.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/publish.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.gitignore +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.python-version +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/LICENSE +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/README.md +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docker-compose.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/dockerfile +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/example.md +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/html2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/md2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/template2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/text2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/entrypoint.sh +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/docker-compose.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/entrypoint.sh +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/html2pic.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/pyproject.toml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/consts.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/signal.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/text.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/conftest.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/resources/test_template_filter.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_deprecated_decorator.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → 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"
|
|
@@ -174,35 +169,62 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
174
169
|
|
|
175
170
|
if plugin_config.htmlrender_browser_executable_path:
|
|
176
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
|
|
177
179
|
else:
|
|
178
180
|
try:
|
|
179
|
-
await check_playwright_env(**kwargs)
|
|
181
|
+
_browser = await check_playwright_env(**kwargs)
|
|
180
182
|
except RuntimeError:
|
|
181
183
|
await install_browser()
|
|
182
|
-
await check_playwright_env(**kwargs)
|
|
183
|
-
|
|
184
|
-
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
184
|
+
_browser = await check_playwright_env(**kwargs)
|
|
185
185
|
|
|
186
186
|
return _browser
|
|
187
187
|
|
|
188
188
|
|
|
189
|
-
async def
|
|
189
|
+
async def shutdown_htmlrender() -> None:
|
|
190
190
|
"""关闭浏览器和 Playwright 实例。"""
|
|
191
191
|
if _browser:
|
|
192
|
-
|
|
193
|
-
|
|
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.")
|
|
194
199
|
if _playwright:
|
|
195
200
|
with suppress_and_log():
|
|
201
|
+
logger.debug("Stopping Playwright...")
|
|
196
202
|
await _playwright.stop()
|
|
203
|
+
logger.info("Playwright stopped.")
|
|
204
|
+
|
|
197
205
|
|
|
206
|
+
async def check_playwright_env(**kwargs) -> Browser:
|
|
207
|
+
"""
|
|
208
|
+
检查Playwright环境,复用_launch方法避免逻辑重复。
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
**kwargs: 传递给`playwright.launch`的关键字参数。
|
|
198
212
|
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
Raises:
|
|
214
|
+
RuntimeError: 如果Playwright环境设置不正确。
|
|
215
|
+
"""
|
|
201
216
|
logger.info("Checking Playwright environment...")
|
|
217
|
+
global _browser, _playwright
|
|
218
|
+
|
|
202
219
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
|
|
205
225
|
except Exception as e:
|
|
226
|
+
await shutdown_htmlrender()
|
|
227
|
+
|
|
206
228
|
raise RuntimeError(
|
|
207
229
|
"Playwright environment is not set up correctly. "
|
|
208
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,7 +56,6 @@ class Config(BaseModel):
|
|
|
38
56
|
htmlrender_connect: Optional[str] = Field(
|
|
39
57
|
default=None, description="通过Playwright协议连接Playwright浏览器的端点地址。"
|
|
40
58
|
)
|
|
41
|
-
|
|
42
59
|
htmlrender_browser_args: Optional[str] = Field(
|
|
43
60
|
default=None, description="Playwright 浏览器启动参数。"
|
|
44
61
|
)
|
|
@@ -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,
|
|
@@ -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
|
|
|
@@ -209,9 +209,9 @@ async def html_to_pic(
|
|
|
209
209
|
"""
|
|
210
210
|
# logger.debug(f"html:\n{html}")
|
|
211
211
|
if "file:" not in template_path:
|
|
212
|
-
raise Exception("template_path
|
|
212
|
+
raise Exception("template_path should be file:///path/to/template")
|
|
213
213
|
async with get_new_page(device_scale_factor, **kwargs) as page:
|
|
214
|
-
page.on("console", lambda msg: logger.debug(f"
|
|
214
|
+
page.on("console", lambda msg: logger.debug(f"[Browser Console]: {msg.text}"))
|
|
215
215
|
await page.goto(template_path)
|
|
216
216
|
await page.set_content(html, wait_until="networkidle")
|
|
217
217
|
await page.wait_for_timeout(wait)
|
|
@@ -266,7 +266,7 @@ async def template_to_pic(
|
|
|
266
266
|
if filters:
|
|
267
267
|
for filter_name, filter_func in filters.items():
|
|
268
268
|
template_env.filters[filter_name] = filter_func
|
|
269
|
-
logger.debug(f"
|
|
269
|
+
logger.debug(f"Custom filter loaded: {filter_name}")
|
|
270
270
|
|
|
271
271
|
template = template_env.get_template(template_name)
|
|
272
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",
|