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.
Files changed (61) hide show
  1. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.pre-commit-config.yaml +5 -5
  2. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/PKG-INFO +2 -1
  3. nonebot_plugin_htmlrender-0.6.8/nonebot_plugin_htmlrender/__init__.py +64 -0
  4. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/browser.py +56 -34
  5. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/config.py +19 -2
  6. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/data_source.py +5 -5
  7. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/install.py +32 -23
  8. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/process.py +39 -11
  9. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/utils.py +62 -1
  10. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/pyproject.toml +2 -1
  11. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_browser.py +77 -23
  12. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_htmlrender.py +3 -3
  13. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_process.py +17 -30
  14. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/uv.lock +146 -120
  15. nonebot_plugin_htmlrender-0.6.7/nonebot_plugin_htmlrender/__init__.py +0 -55
  16. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.dockerignore +0 -0
  17. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/codecov.yml +0 -0
  18. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/docker_image.yml +0 -0
  19. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.github/workflows/publish.yml +0 -0
  20. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.gitignore +0 -0
  21. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/.python-version +0 -0
  22. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/LICENSE +0 -0
  23. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/README.md +0 -0
  24. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docker-compose.yaml +0 -0
  25. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/dockerfile +0 -0
  26. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/example.md +0 -0
  27. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/html2pic.png +0 -0
  28. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/md2pic.png +0 -0
  29. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/template2pic.png +0 -0
  30. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/docs/text2pic.png +0 -0
  31. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/entrypoint.sh +0 -0
  32. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/docker-compose.yaml +0 -0
  33. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/entrypoint.sh +0 -0
  34. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/__init__.py +0 -0
  35. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/html2pic.html +0 -0
  36. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/markdown.css +0 -0
  37. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/mystyle.css +0 -0
  38. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/progress.html.jinja2 +0 -0
  39. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/templates/text.html +0 -0
  40. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/plugins/render/utils.py +0 -0
  41. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/example/pyproject.toml +0 -0
  42. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/consts.py +0 -0
  43. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/signal.py +0 -0
  44. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
  45. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
  46. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
  47. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
  48. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
  49. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
  50. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
  51. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/text.css +0 -0
  52. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/nonebot_plugin_htmlrender/templates/text.html +0 -0
  53. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/__init__.py +0 -0
  54. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/conftest.py +0 -0
  55. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/resources/test_template_filter.png +0 -0
  56. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/markdown.css +0 -0
  57. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/mystyle.css +0 -0
  58. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/progress.html.jinja2 +0 -0
  59. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/templates/text.html +0 -0
  60. {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.6.8}/tests/test_deprecated_decorator.py +0 -0
  61. {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.8.2
10
+ rev: v0.12.12
11
11
  hooks:
12
- - id: ruff
13
- args: [--fix]
14
- stages: [pre-commit]
15
12
  - id: ruff-format
16
- stages: [pre-commit]
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.7
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 proxy_settings, suppress_and_log, with_lock
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 init_browser(**kwargs)
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"正在使用 CDP 连接 Chromium({plugin_config.htmlrender_connect_over_cdp})"
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"正在使用 Playwright 协议连接 {browser_type}({plugin_config.htmlrender_connect})"
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 start_browser(**kwargs) -> Browser:
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 shutdown_browser()
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 shutdown_browser() -> None:
189
+ async def shutdown_htmlrender() -> None:
190
190
  """关闭浏览器和 Playwright 实例。"""
191
191
  if _browser:
192
- with suppress_and_log():
193
- await _browser.close()
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
- async def check_playwright_env(**kwargs):
200
- """Check Playwright environment."""
213
+ Raises:
214
+ RuntimeError: 如果Playwright环境设置不正确。
215
+ """
201
216
  logger.info("Checking Playwright environment...")
217
+ global _browser, _playwright
218
+
202
219
  try:
203
- async with async_playwright() as p:
204
- await getattr(p, plugin_config.htmlrender_browser).launch(**kwargs)
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[str] = Field(
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("必须输入 md md_path")
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"加载自定义过滤器 {filter_name}")
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 应该为 file:///path/to/template")
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"浏览器控制台: {msg.text}"))
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"加载自定义过滤器 {filter_name}")
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.info(f"Using Mirror source: {best_mirror.name} ({best_mirror.url})")
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
- char = await stream.read(1)
134
- if not char:
135
+ line_data = await stream.readline()
136
+ if not line_data:
135
137
  break
136
138
 
137
- if char == b"\r":
138
- continue
139
+ line = line_data.decode().strip()
139
140
 
140
- line = char + await stream.readuntil(b"\n")
141
- text = line.decode().strip()
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 "|" in text and "%" in text:
144
- if text != last_progress:
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
- logger.info(f"{line.strip()}")
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
- logger.warning(f"Install error: {line.strip()}")
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
- returncode = process.returncode
210
+ return_code = process.returncode
203
211
 
204
- if returncode != 0:
205
- return False, f"Exited with code {returncode}"
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
- return await asyncio.create_subprocess_exec(
100
- *args,
101
- cwd=cwd,
102
- stdin=stdin,
103
- stdout=stdout,
104
- stderr=stderr,
105
- creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if WINDOWS else 0,
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
- process.terminate()
164
-
165
- await process.wait()
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("关闭 playwright 时发生错误。")
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.7"
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",